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.
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
andGroup:xyz789.events
queries were not updated with the new event; this is a caching problem.
To work around it, we have 2 options:
- Blow everything in the cache away after every mutation with
client.resetStore()
(the sledge hammer approach) - 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 renderingallEvents
shouldn't need to know or care there is a component calling the queryallGroups
which could result in this cache problem.
This module introduces a 3rd way of solving the issue:
- 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
andGroup:xyz789.events
have been cleared as they referred to the typeEvent
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
, andGroup: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 Event
s.
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.