Apollo-client: Local state resolvers with reactive data

Created on 5 Nov 2019  路  12Comments  路  Source: apollographql/apollo-client

Hi 馃憢

I'm playing around with Apollo's Local State Management feature and I found a strange behaviour that has not been made clear in the documentation. Could you help me understand if this is a bug, or if it's by design and if there's a workaround?

Here it is:

  • I have an Apollo Server with these typeDefs:
  type Query {
    greetings: [Greeting]
  }

  type Greeting {
    word: String
  }
  • the Apollo client is set with a local resolver and a local state:
const client = new ApolloClient({
  // ...
  defaultOptions: {
    query: {
      fetchPolicy: "network-only"
    },
  },
  resolvers: {
    Greeting: {
      name: (parent, args, { client }) => {
        const state = client.readQuery({
          query: gql`
            query StateQuery {
              localState {
                value
              }
            }
          `
        });

        return state.localState.value;
      }
    }
  }
});

client.writeData({
  data: {
    localState: {
      __typename: "LocalState",
      value: "John"
    }
  }
});

With this setup, everything is ok on the first query with react-apollo hook:

const { data } = useQuery(gql`
  query ComponentQuery {
    localState @client {
      value
    }
    greetings {
      name @client(always: true)
      word
    }
  }
`);

console.log(data);
/* {
  "localState": {
    "value": "John",
    "__typename": "LocalState"
  },
  "greetings": [
    {
      "name": "John",
      "word": "Hello",
      "__typename": "Greeting"
    },
    {
      "name": "John",
      "word": "Welcome",
      "__typename": "Greeting"
    }
  ]
} */

But when I update the local state with:

client.writeQuery({
  query: STATE_QUERY,
  data: {
    localState: { __typename: "LocalState", value: "Foo" }
  }
});

The previous query is correctly re-run, but the local field name has not been updated:

console.log(data);
/* {
  "localState": {
    "value": "Foo",
    "__typename": "LocalState"
  },
  "greetings": [
    {
      "name": "John",
      "word": "Hello",
      "__typename": "Greeting"
    },
    {
      "name": "John",
      "word": "Welcome",
      "__typename": "Greeting"
    }
  ]
} */

You can find a Sandbox set up here: https://codesandbox.io/s/apollo-local-state-management-5dy2f?fontsize=14&module=%2Fsrc%2FApolloProvider.tsx

Is it intended? Do you plan on supporting this kind of feature?

Thank you for your help!

鈦夛笍 question 馃彄 awaiting-response 馃洭 fixed-in-prerelease

Most helpful comment

@freshollie OK I understand now, thanks 馃檹

I'd be nice if Apollo treated readQuery inside of a local resolver as a "watch query", just like useQuery inside of React component.

My feature request is if the source of readQuery changes _and_ the local resolver is still in use by some React component's useQuery, then the local resolver would automatically re-run and its results get sent to the React component.

This way I wouldn't need to do all of the bookkeeping and updating myself (i.e. which React components are active, which fragments they use, which local resolvers of those fragments are used, and how to recalculate the results of all those local resolvers).

All 12 comments

The greetings resolver has been cached, so any subsequent queries will not execute the resolver. To fix with behaviour you can turn off the caching policy no-cache which is not recommended.

If you want to update the greetings local query, you have to write the new version of that query into the cache.

I too also didn't understand this, and struggled for days reading the documentation and just trying it out until I found the issue.

Edit: you can use @client(always: true) too.

@freshollie Can give an example of writing a new version of the query into the cache?

Also, it seems like @client(always: true) is already in @TiuSh's example but it has no effect...

@dcecile Please see: https://codesandbox.io/s/apollo-local-state-management-zrwel

The writeQuery should not only update the localState, but also update all greetings objects in the cache in order to achieve the desired effect.

The reason the @client(always: true) doesn't work is because the overall greetings query has already been cached, so any subsequent queries are not going to execute the name resolver.

@freshollie OK I understand now, thanks 馃檹

I'd be nice if Apollo treated readQuery inside of a local resolver as a "watch query", just like useQuery inside of React component.

My feature request is if the source of readQuery changes _and_ the local resolver is still in use by some React component's useQuery, then the local resolver would automatically re-run and its results get sent to the React component.

This way I wouldn't need to do all of the bookkeeping and updating myself (i.e. which React components are active, which fragments they use, which local resolvers of those fragments are used, and how to recalculate the results of all those local resolvers).

Thanks for your help @freshollie!
Playing around with your exemple I now fully understand the issue... But I was expecting the same as @dcecile:

if the source of readQuery changes and the local resolver is still in use by some React component's useQuery, then the local resolver would automatically re-run and its results get sent to the React component.

That would be really nice! And that would finally make Apollo the perfect fit for our local state management!

FYI, @sirctseb posted to StackOverflow with an alternate workaround, to use a top-level watchQuery subscription:

client.watchQuery({ query: sourceQuery }).subscribe(value =>
  client.writeData({ data: { resolvedDevice: doStuffWithTheData(value.data.selectedDevice) } });

(He explains that this solution needs extra bookkeeping, potentially automated using a HOC, to make sure that the watchQuery only runs when it's actually needed by a component.)

In Apollo Client 3.0, a better way to implement this behavior is to us a custom read function, and remove the Greeting entry from your resolvers:

import { InMemoryCache } from "@apollo/client";

const LocalStateValueQuery = gql`
  query StateQuery {
    localState {
      value
    }
  }
`;

const cache = new InMemoryCache({
  typePolicies: {
    Greeting: {
      fields: {
        name(existingName: string) {
          return existingName || cache.readQuery({
            query: LocalStateValueQuery,
          })?.localState?.value;
        },
      },
    },
  },
});

Because the name function reads the LocalStateValueQuery, its result will be automatically invalidated when new data is written for the localState field.

If you want to get a little fancier, you could give your LocalState singleton object an ID, which allows creating a reference to it, so you can read its fields without calling cache.readQuery:

const cache = new InMemoryCache({
  typePolicies: {
    LocalState: {
      // Indicate that the ID of the singleton LocalState object does not depend
      // on any of its fields, and is therefore constant: "LocalState:{}".
      keyFields: [],
    },
    Greeting: {
      fields: {
        name(existingName: string, { readField, toReference }) {
          return existingName || readField("value", toReference({
            __typename: "LocalState",
          }));
        },
      },
    },
  },
});

This pattern is also fully reactive, and should be much faster because it skips the overhead of cache.readQuery.

While I stand by my recommendations above, I'm not entirely happy with either one.

Here's the API I would really like to use:

const cache = new InMemoryCache({
  typePolicies: {
    Greeting: {
      name() {
        return nameVar.get();
      }
    }
  }
});

// A default value can be provided when the variable is created,
// which also allows TypeScript to enforce the type.
const nameVar = cache.variable("John");

// The variable can be updated anywhere, like so:
nameVar.set("Ben");

Ok, here's my take on the reactive local variable idea: https://github.com/apollographql/apollo-client/pull/5799

https://github.com/apollographql/apollo-client/pull/5799 was merged, so I'll close this off. Thanks!

Nice! Thanks @benjamn for your work, can't wait to try it!

Was this page helpful?
0 / 5 - 0 ratings