Case 1: Payload is using common entities
Simply put, the Apollo client's cache keeps everything that's received from queries and mutations, though the schema needs to include id: ID!
fields and any query needs to use both the id
and __typename
fields on relevant nodes for the client to know which part of the cache to update.
This assumes that the mutation payload is common data from the schema that can be retrieved through a normal query. This is the best case scenario.
Given the following schema on the server:
type User {
id: ID!
phoneNumber: String!
}
type Query {
user(id: String!): User!
}
type UpdateUserPayload {
user: User!
}
type Mutation {
updateUser(id: String!, phoneNumber: String!): UpdateUserPayload!
}
And assuming a cache is used on the client:
import { InMemoryCache, ApolloClient } from '@apollo/client';
const client = new ApolloClient({
// ...other arguments...
cache: new InMemoryCache(options)
});
The cache generates a unique ID for every identifiable object included in the response.
The cache stores the objects by ID in a flat lookup table.
Whenever an incoming object is stored with the same ID as an existing object, the fields of those objects are merged.
- If the incoming object and the existing object share any fields, the incoming object overwrites the cached values for those fields.
- Fields that appear in only the existing object or only the incoming object are preserved.
Normalization constructs a partial copy of your data graph on your
client, in a format that's optimized for reading and updating the
graph as your application changes state.
The client's mutation should be
mutation UpdateUserPhone($phoneNumber: String!, $id: String!) {
updateUser(id: $id, phoneNumber: $phoneNumber) {
user {
__typename # Added by default by the Apollo client
id # Required to identify the user in the cache
phoneNumber # Field that'll be updated in the cache
}
}
}
Then, any component using this user through the same Apollo client in the app will be up to date automatically. There's nothing special to do, the client will use the cache by default and trigger renders whenever the data changes.
import { gql, useQuery } from '@apollo/client';
const USER_QUERY = gql`
query GetUser($id: String!) {
user(id: $id) {
__typename
id
phoneNumber
}
}
`;
const UserComponent = ({ userId }) => {
const { loading, error, data } = useQuery(USER_QUERY, {
variables: { id: userId },
});
if (loading) return null;
if (error) return `Error! ${error}`;
return <div>{data.user.phoneNumber}</div>;
}
The fetchPolicy
option defaults to cache-first
.
Case 2: Payload is custom data specific to the mutation
If the data is in fact not available elsewhere in the schema, it won't be possible to use the Apollo cache automatically as explained above.
Use another state management solution
A couple options:
Here's an example from the Apollo GraphQL documentation using the localStorage
:
const [login, { loading, error }] = useMutation(LOGIN_USER, {
onCompleted({ login }) {
localStorage.setItem('token', login.token);
localStorage.setItem('userId', login.id);
}
});
This is a pure Apollo GraphQL solution since the client is also a state management library, which enables useful developer tooling and helps reason about the data.
Create a local schema.
// schema.js
export const typeDefs = gql`
type DataToKeep {
# anything here
}
extend type Query {
dataToKeep: DataToKeep # probably nullable?
}
`;
Initialize a custom cache
// cache.js
export const dataToKeepVar = makeVar(null);
export const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
dataToKeep: {
read() {
return dataToKeepVar();
}
},
}
}
}
});
Apply the schema override at the client's initialization
import { InMemoryCache, Reference, makeVar } from '@apollo/client';
import { cache } from './cache';
import { typeDefs } from './schema';
const client = new ApolloClient({
cache,
typeDefs,
// other options like, headers, uri, etc.
});
Keep track of the changes in the mutation:
const [myMutation, { data, errors, loading }] = useMutation(MY_MUTATION, {
onCompleted({ myMutation }) {
if (myMutation && myMutation.dataToKeep)
dataToKeepVar(myMutation.dataToKeep);
}
});
Then, query the @client
field.
import { gql, useQuery } from '@apollo/client';
const DATA_QUERY = gql`
query dataToKeep {
dataToKeep @client {
# anything here
}
}
`;
const AnyComponent = ({ userId }) => {
const { loading, error, data } = useQuery(DATA_QUERY);
if (loading) return null;
if (error) return `Error! ${error}`;
return <div>{JSON.stringify(data.dataToKeep)}</div>;
}
See also the documentation on managing local state.