npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@keystonejs/apollo-helpers

v5.3.2

Published

Utilities to make using react-apollo + KeystoneJS easier.

Downloads

145

Readme

Apollo helpers

This is the last active development release of this package as Keystone 5 is now in a 6 to 12 month active maintenance phase. For more information please read our Keystone 5 and beyond post.

View changelog

A set of functions and components to ease using Apollo with Keystone.

Installation

yarn add @keystonejs/apollo-helpers

Usage

Minimal example

import gql from 'graphql-tag';
import React from 'react';
import ReactDOM from 'react-dom';
import { HttpLink } from 'apollo-link-http';
import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { ApolloProvider } from 'react-apollo';
import { Query, KeystoneProvider } from '@keystonejs/apollo-helpers';

const client = new ApolloClient({
  link: new HttpLink({ uri: '...' }),
  cache: new InMemoryCache(),
});

const App = () => (
  <ApolloProvider client={client}>
    <KeystoneProvider>
      <Query query={gql`...`}>
        {({ data }) => (
          <pre>{JSON.stringify(data)}</pre>
        )}
      </Query>
    </KeystoneProvider>
  </ApolloProvider>
);

ReactDOM.render(<App />, document.getElementById('root'));

Complete example

import gql from 'graphql-tag';
import React from 'react';
import ReactDOM from 'react-dom';
import { HttpLink } from 'apollo-link-http';
import { ApolloLink } from 'apollo-link';
import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { ApolloProvider } from 'react-apollo';
import { withClientState } from 'apollo-link-state';
import {
  Query,
  Mutation,
  KeystoneProvider,
  injectIsOptimisticFlag,
  flattenApollo,
} from '@keystonejs/apollo-helpers';

const cache = new InMemoryCache();

// Without this, we don't get the _isOptimistic flag on our datatypes
const stateLink = withClientState(
  injectIsOptimisticFlag({
    resolvers: {
      /* ... Add your local state resolvers here */
    },
    defaults: {
      /* ... Add your resolver defaults here */
    },
    cache,
  })
);

const client = new ApolloClient({
  link: ApolloLink.from([stateLink, new HttpLink({ uri: /* ... */ })]),
  cache: cache,
});

const GET_FOO_QUERY = gql`...`
const UPDATE_FOO_MUTATION = gql`...`
const ADD_FOO_MUTATION = gql`...`

// NOTE: This format only works with the `Query`/`Mutation` components from this
// module, not Apollo's `Query`/`Mutation` queries. For those, you'll have to
// wrap each item in a function ({ render }) => <Query ..>{render}</Query>
const GraphQL = flattenApollo({
  foo: <Query query={GET_FOO_QUERY}>,
  // Note the `invalidateTypes` define the GraphQL types to invalidate within
  // the Apollo cache upon mutation
  updateFoo: <Mutation mutation={UPDATE_FOO_MUTATION} invalidateTypes="Foo">,
  addFoo: <Mutation mutation={ADD_FOO_MUTATION} invalidateTypes="Foo">,
})

// Calling `updateFoo` or `addFoo` will trigger the cache invalidation, and so
// will re-execute the `Query` (as it has queried a `Foo` type in the past),
// which will trigger a re-render here.
// In a more complicated app, _all_ `Query`'s which have queried a `Foo` type
// will also be re-rendered without having to wire them up to these particular
// mutations (decoupling FTW!)
const App = () => (
  <ApolloProvider client={client}>
    <KeystoneProvider>
      <GraphQL>
        {({ foo, updateFoo, addFoo }) => (
          <div>
            Foo: <pre>{JSON.stringify(foo, null, 2)}</pre>
            <button onClick={updateFoo}>Update</button>
            <button onClick={addFoo}>Add</button>
          </div>
        )}
      </GraphQL>
    </KeystoneProvider>
  </ApolloProvider>
);

ReactDOM.render(<App />, document.getElementById('root'));

Motivation

Let's start with an example of using Apollo's Query / Mutation components given the following GraphQL schema:

type Event {
  id: ID!
}
type Group {
  id: ID!
  events: [Event]
}
type Meta {
  count: Integer
}
type Query {
  allEvents: [Event]
  _allEventsMeta: Meta
  allGroups: [Group]
  _allGroupsMeta: Meta
}
type Mutation {
  addEvent(group: ID!): Event
}

When we perform the following query using Apollo's <Query> component:

<Query
  query={`
    query {
      allEvents {
        id
      }
      allGroups {
        events {
          id
        }
      }
    }
  `}
/>

Apollo will cache the result of allEvents/allGroups, plus the individual events, which looks roughly like:

{
  "allEvents": ["abc123"],
  "allGroups": ["xyz789"],
  "Event:abc123": { "id": "abc123" },
  "Group:xyz789": { "id": "xyz789", "events": ["abc123"] }
}

Now, when we do a mutation using Apollo's <Mutation> component:

<Mutation
  mutation={`
    mutation {
      addEvent(group: "xyz789") {
        id
      }
    }
  `}
/>

We've created a new Event with data { id: "def456" }, which Apollo will also cache:

{
  "allEvents": ["abc123"],
  "allGroups": ["xyz789"],
  "Event:abc123": { "id": "abc123" },
  "Event:def456": { "id": "def456" },
  "Group:xyz789": { "id": "xyz789", "events": ["abc123"] }
}

Notice the allEvents and Group:xyz789.events queries were not updated with the new event; this is a caching problem.

To work around it, we have 2 options:

  1. Blow everything in the cache away after every mutation with client.resetStore() (the sledge hammer approach)
  2. Use Apollo's writeQuery method to update the cache after each mutation. This requires every place where we perform a <Mutation> to know about all other possible queries that could be affected! This leads to tightly coupled components and overly verbose code. Eg; The component rendering allEvents shouldn't need to know or care there is a component calling the query allGroups which could result in this cache problem.

This module introduces a 3rd way of solving the issue:

  1. After every mutation, clear only a subset of the data from the cache; that which relates to the mutated type.

What does that mean? Let's continue using the above example but with this module's <Mutation> component;

<Mutation
  invalidateTypes="Event"
  mutation={`
    mutation {
      addEvent(group: "xyz789") {
        id
      }
    }
  `}
/>

Notice the new invalidateTypes="Event" prop we've passed - this instructs on which GraphQL type(s) to clear from the Apollo cache

Will immediately result in the following cache:

{
  "allGroups": ["xyz789"],
  "Event:abc123": { "id": "abc123" },
  "Event:def456": { "id": "def456" },
  "Group:xyz789": { "id": "xyz789" }
}

Notice allEvents and Group:xyz789.events have been cleared as they referred to the type Event

Which, when using this module's <Query> component, will re-load the now removed data from the server, resulting in a final cache of:

{
  "allEvents": ["abc123", "def456"],
  "allGroups": ["xyz789"],
  "Event:abc123": { "id": "abc123" },
  "Event:def456": { "id": "def456" },
  "Group:xyz789": { "id": "xyz789", "events": ["abc123", "def456"] }
}

Now everything's up to date and we didn't have to use writeQuery or couple any of our components.

Why is this coupled to Keystone?

Let's continue the example above with another query:

<Query
  query={`
    query {
      allEvents {
        id
      }
      _allEventsMeta {
        count
      }
    }
  `}
/>

Would result in the cache:

{
  "allEvents": ["abc123", "def456"],
  "_allEventsMeta": { "count": 2 },
  "allGroups": ["xyz789"],
  "Event:abc123": { "id": "abc123" },
  "Event:def456": { "id": "def456" },
  "Group:xyz789": { "id": "xyz789", "events": ["abc123", "def456"] }
}

Then we add another Event (using an Apollo <Mutation>):

<Mutation
  mutation={`
    mutation {
      addEvent(group: "xyz789") {
        id
      }
    }
  `}
/>

Now the cache is:

{
  "allEvents": ["abc123", "def456"],
  "_allEventsMeta": { "count": 2 },
  "allGroups": ["xyz789"],
  "Event:abc123": { "id": "abc123" },
  "Event:def456": { "id": "def456" },
  "Event:hij098": { "id": "hij098" },
  "Group:xyz789": { "id": "xyz789", "events": ["abc123", "def456"] }
}

Not only are allEvents & Group:xyz789 out of date, but so is _allEventsMeta (it should be { count: 3 }).

If we were to use this module's <Mutation> component, but decoupled from Keystone, the cache at this point would be:

{
  "_allEventsMeta": { "count": 2 },
  "allGroups": ["xyz789"],
  "Event:abc123": { "id": "abc123" },
  "Event:def456": { "id": "def456" },
  "Event:hij098": { "id": "hij098" },
  "Group:xyz789": { "id": "xyz789" }
}

Notice allEvents, and Group:xyz789.events are cleared, but _allEventsMeta is not

This example highlights the limits of other approaches (see below for possible workarounds / other solutions to this).

In swoops Keystone to the rescue! 🦅

We do know the related type information within Keystone! It's a walled garden which we control, so can extract further information such as _allEventsMeta is a query that relates to Events.

So, using this module's <Mutation> component:

<Mutation
  invalidateTypes="Event"
  mutation={`
    mutation {
      addEvent(group: "xyz789") {
        id
      }
    }
  `}
/>

Will result in an immediate cache of:

{
  "allGroups": ["xyz789"],
  "Event:abc123": { "id": "abc123" },
  "Event:def456": { "id": "def456" },
  "Event:hij098": { "id": "hij098" },
  "Group:xyz789": { "id": "xyz789" }
}

Notice allEvents, Group:xyz789.events, and now _allEventsMeta have been cleared

Which, when using this module's <Query> component, will re-load the now removed data from the server, resulting in a final cache of:

{
  "allEvents": ["abc123", "def456", "hij098"],
  "_allEventsMeta": { "count": 3 },
  "allGroups": ["xyz789"],
  "Event:abc123": { "id": "abc123" },
  "Event:def456": { "id": "def456" },
  "Event:hij098": { "id": "hij098" },
  "Group:xyz789": { "id": "xyz789", "events": ["abc123", "def456", "hij098"] }
}

Can we decouple from Keystone?

If we only cared about queries that explicitly relate to a given type, then we can scan the GraphQL AST / Introspection query to get all the correct queries. This will miss queries such as _allEventsMeta.

If we push a bit of the work on linking up queries + types to the developer, we could rely on directives so our GraphQL schema becomes:

type Query {
  allEvents: [Event] @relatedToType(type: "Event")
  _allEventsMeta: Meta @relatedToType(type: "Event")
  allGroups: [Group] @relatedToType(type: "Group")
  _allGroupsMeta: Meta @relatedToType(type: "Group")
}

Note the @relatedToType() directive

Which could be simplified by combining it with GraphQL AST scanning to become:

type Query {
  allEvents: [Event]
  _allEventsMeta: Meta @relatedToType(type: "Event")
  allGroups: [Group]
  _allGroupsMeta: Meta @relatedToType(type: "Group")
}

If we went with this method, we could automatically inject that directive into Keystone generated GraphQL schemas.