1

I'm using Apollo Client in a React app and I need to do a mutation then keep the returned data for later use (but I won't have access to the variables ), do I have to use a another state management solution or can we do this in Apollo?

I've read about doing this with query but not mutation.

Here's my code so far

// Mutation
const [myMutation, { data, errors, loading }] = useMutation(MY_MUTATION, {
    onCompleted({ myMutation }) {
      console.log('myMutation: ', myMutation.dataToKeep);
      if (myMutation && myMutation.dataToKeep)
        SetResponse(myMutation.dataToKeep);
    },
    onError(error) {
      console.error('error: ', error);
    },
  });

//How I call it

  onClick={() => {
    myMutation({
      variables: {
        input: {
          phoneNumber: '0000000000',
          id: '0000',
        },
      },
    });
  }}

edit:

here is the mutation

export const MY_MUTATION = gql`
  mutation MyMutation($input: MyMutationInput!) {
    myMutation(input: $input) {
      dataToKeep
      expiresAt
    }
  }
`;

and the schema for this mutation

MyMutationInput:
  phoneNumber: String!
  id: String!

MyMutationPayload:
  dataToKeep
  expiresAt
  
Emile Bergeron
  • 17,074
  • 5
  • 83
  • 129
Mel
  • 625
  • 9
  • 25
  • 1
    what version of apollo-client are you using? and also what do you mean by later use? you mean like a global state persisted in the app or more like a state that needs to be shared across different components? – Manuel Pamplona Dec 01 '20 at 18:20
  • @ManuelPamplona I'm using 3.2.9 – Mel Dec 01 '20 at 19:10

1 Answers1

1

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)
});
  1. The cache generates a unique ID for every identifiable object included in the response.

  2. The cache stores the objects by ID in a flat lookup table.

  3. 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);
  }
});

Define a client-side schema

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.

  1. Create a local schema.

    // schema.js
    export const typeDefs = gql`
      type DataToKeep {
        # anything here
      }
    
      extend type Query {
        dataToKeep: DataToKeep # probably nullable?
      }
    `;
    
  2. Initialize a custom cache

    // cache.js
    export const dataToKeepVar = makeVar(null);
    
    export const cache = new InMemoryCache({
      typePolicies: {
        Query: {
          fields: {
            dataToKeep: {
              read() {
                return dataToKeepVar();
              } 
            },
          }
        }
      }
    });
    
  3. 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.
    });
    
  4. 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);
      }
    });
    
  5. 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.

Emile Bergeron
  • 17,074
  • 5
  • 83
  • 129
  • "Then, any component using this user through the same Apollo client in the app will be up to date automatically." And how would I access this user in the cache? – Mel Dec 01 '20 at 20:15
  • @Mel nothing special is needed, I've added a simple example that would use the cache by default. – Emile Bergeron Dec 01 '20 at 20:30
  • When I create a new query as per your example I get "message": "Field 'myMutation' doesn't exist on type 'Query'", because there's not query of this name in the API. What am I missing? – Mel Dec 01 '20 at 22:55
  • @Mel please include a [mcve] in your question, as there's not enough details to pinpoint the problem. It looks like you're mixing mutations and queries, which isn't going to work. – Emile Bergeron Dec 01 '20 at 22:58
  • I have a Mutation and I'm trying to access the returned data from this mutation without having to call the mutation again (because I won't have access to the props). So I think what I'm trying to achieve is access the cache of Apollo. Sorry if my question wasn't clear enough, I'm still learning how to properly use Apollo – Mel Dec 01 '20 at 23:28
  • @Mel please add relevant details to your question (click the edit button under the description). Things like the actual mutation and its payload, the relevant parts of the schema, the data you want to keep, etc – Emile Bergeron Dec 01 '20 at 23:55
  • I've added the informations that I have. I should mention that this mutation works as intended, it's only the cache reading that's giving me trouble – Mel Dec 02 '20 at 00:09
  • 1
    @Mel ok, after digging a little, I've included 2 additional solutions: using another state management solution, like you suggested, and using a local state since the Apollo Client lib is also a state management library. – Emile Bergeron Dec 02 '20 at 00:54
  • 1
    I ended up using context, but your answer is exactly what I wanted to do in the beginning, merci – Mel Dec 02 '20 at 07:45