apollographql / apollo-client

:rocket:  A fully-featured, production ready caching GraphQL client for every UI framework and GraphQL server.
https://apollographql.com/client
MIT License
19.38k stars 2.66k forks source link

Cannot update a field in cache that has not been queried yet #7814

Closed SeungsuKim closed 3 years ago

SeungsuKim commented 3 years ago

Intended outcome:

Consider an example API with following schema:

type Country {
  code: ID!
  name: String!
  states: [State!]!
}

type State {
  code: ID!
  name: String!
  country: Country!
}

I want to add another state to Country in the cache. Thus I wrote the following function:

const addState = () => {
    client.cache.modify({
      id: `Country:{"code":"US"}`,
      fields: {
        states(existingStateRefs = [], { canRead }) {
          console.log("ADDING STATE!");
          const newStateRef = client.cache.writeFragment({
            data: {
              __typename: "State",
              code: "Seoul",
              name: "Seoul",
              country: { __ref: `Country:{"code":"US"}` }
            },
            fragment: gql`
              fragment NewState on State {
                code
                name
                country
              }
            `
          });

          console.log("NEW REF", canRead(newStateRef));

          return canRead(newStateRef)
            ? [...existingStateRefs, newStateRef]
            : existingStateRefs;
        }
      }
    });
  };

When I have queried the following query, everything works fine.

const COUNTRY_QUERY = gql`
  query country($code: ID!) {
    country(code: $code) {
      code
      name
      states {
          code
          name
       }
    }
  }
`;

Actual outcome:

However, if I query the following query, which does not have states field,

const COUNTRY_QUERY = gql`
  query country($code: ID!) {
    country(code: $code) {
      code
      name
    }
  }
`;

The client.cache.modify.fields's states function does not even executed without any error. I expected the state function to be executed with existingStateRef argument of undefined.

How to reproduce the issue:

I've reproduced the issue on codesandbox https://codesandbox.io/s/apollo-client-cache-error-reproduction-zx62j?file=/src/App.js:582-701

Versions

System:
    OS: macOS 10.15.6
  Binaries:
    Node: 14.15.1 - /usr/local/bin/node
    Yarn: 1.22.10 - /usr/local/bin/yarn
    npm: 6.14.8 - /usr/local/bin/npm
  Browsers:
    Chrome: 89.0.4389.82
    Safari: 14.0
  npmPackages:
    @apollo/client: ^3.3.7 => 3.3.7 
    apollo-upload-client: ^14.1.3 => 14.1.3 
benjamn commented 3 years ago

@SeungsuKim This is one of the downsides of cache.modify: it can only modify existing field values, so you can't use it to add new fields.

Try using a combination of readFragment and writeFragment instead?

const idUS = cache.identify({
  __typename: "Country",
  code: "US",
});

const dataUS = cache.readFragment({ id: idUS, fragment }));

const newStateRef = cache.writeFragment({ ... });

cache.writeFragment({
  id: idUS,
  fragment,
  data: {
    ...dataUS,
    states: [
      ...dataUS.states,
      newStateRef,
    ],
  },
});

If you need the canRead helper, you should be able to use cache.store.canRead.

Happy to answer questions if you get stuck on anything!

SeungsuKim commented 3 years ago

@benjamn I've solved the problem as follows. I'm trying to add a new Message data received by subscription to a messages field of type Chat. Is there any more elegant and concise way to do this?

const chatCache = cache.readFragment<{ messages: { id: string }[] }>({
  id: `Chat:${newMessage.chatId}`,
  fragment: gql`
    fragment ExistingMessages on Chat {
      messages {
        id
      }
    }
  `,
});

cache.writeFragment({
  id: `Chat:${newMessage.chatId}`,
  fragment: gql`
    fragment NewMessages on Chat {
      messages
    }
  `,
  data: {
    messages: [
      ...(chatCache?.messages.map(message => ({
        __ref: `Message:${message.id}`,
      })) || []),
      { __ref: `Message:${newMessage.id}` },
    ],
  },
});
SeungsuKim commented 3 years ago

@benjamn Also, if the cache.modify function is intended to not modify non-existing fields, I think it should be documented and throw a warning when such cases happen. I took so much time to figure out that cache.modify does not work with non-existing fields. Is there any documentation about this behavior?

benjamn commented 3 years ago

@SeungsuKim When you're using writeFragment, you don't have to worry about { __ref } reference objects (like you do when you're using cache.modify), so here's how I would simplify that code:

const chatMessagesFragment = gql`
  fragment Messages on Chat {
    messages {
      id
    }
  }
`;

const newMessageId = cache.identify(newMessage);

const chatCache = cache.readFragment<{ messages: { id: string }[] }>({
  id: newMessageId,
  fragment: chatMessagesFragment,
});

cache.writeFragment({
  id: newMessageId,
  fragment: chatMessagesFragment,
  data: {
    messages: [
      ...(chatCache ? chatCache.messages : []),
      newMessage,
    ],
  },
});

In case you haven't seen it already, here's the documentation we have for cache.modify. I agree it could be more explicit about the inability to add new fields (cc @StephenBarlow). Although cache.modify doesn't warn when nothing is modified, it does return a boolean to indicate whether any modifications happened, in case that's useful.

hwillson commented 3 years ago

It doesn't sound like there is an outstanding issue here, so closing. Thanks!