4
votes

I'm currently working on a react app, that uses a GraphQL backend and has additional local state. I am using a resolver to resolve a local field that changes over time but the resolver is only triggered once.

I tried to use cache.readQuery to re-run the query in case the local field changes, but it does not seem to work as I expected.

export const resolvers = {
  Query: {
    resolvedDevice: (obj, args, { cache }, info) => {
      const data = cache.readQuery({
        query: gql`
          query {
            selectedDevice @client
          }
        `
      });

      // do stuff with the data
    }
  },
  Mutation: {
    selectDevice: (_, { id }, { cache }) => {
      cache.writeData({ data: { selectedDevice: id } });
    }
  }
};

const query = gql`
  query GetResolvedDevice {
    resolvedDevice @client
  }
`;

In this case "resolvedDevice" within the resolver is only executed once, even if I mutate the cache via the mutation "selectDevice". I expected that when changing the local state via mutation the resolvers also runs again, because cache is changing.

Here is the code that executes the query:

const ModalContainer = props => {
  const { loading, error, data } = useQuery(query);

  if (loading || error) {
    return null;
  }

  return (
    <Modal
      device={data.resolvedDevice}
    />
  );
};

And in this component I am running a mutation on selectedDevice:

export const SELECT_DEVICE = gql`
  mutation SelectDevice($id: String!) {
    selectDevice(id: $id) @client
  }
`;

const DevicesNoGeoContainer = () => {
  const [selectDevice] = useMutation(SELECT_DEVICE);

  return (
    <DevicesNoGeo
      onGeoClick={id => {
        selectDevice({ variables: { id } });
      }}
    />
  );
};
1
Queries will not update automatically on their own. Can you post the code where you're running your query/mutation? And to be clear, the queries involved are all local, or are you fetching data from your back end?Chris B.
Does this help?Christoph Weise-Onnen

1 Answers

3
votes

Apollo knows to update watched queries whose fields are drawn from the cache when those values in the cache change. In this case, the field in the query is fulfilled by a local resolver instead. That means there's no cache entry for Apollo to subscribe and react to for that specific query. So the first query is fulfilled and you won't get any updates to the query unless you trigger it explicitly with refetch on the hook result.

One way we're looking to solve this problem is by "persisting" derived fields in the cache and using the cache-fulfilled fields in component queries. We can do this by explicitly watching the source field (selectedDevice) and, in the handler, writing the derived field (resolvedDevice) back to the cache (I'll keep using your field name, though you might consider renaming it if you go this route, as it seems to be named for the way it is defined).

A Proof-of-Concept

export const resolvers = {
  Mutation: {
    selectDevice: (_, { id }, { cache }) => {
      cache.writeData({ data: { selectedDevice: id } });
    }
  }
};

const client = new ApolloClient({
  resolvers
});

const sourceQuery = gql`
  query {
    selectedDevice @client
  }`;

// watch the source field query and write resolvedDevice back to the cache at top-level
client.watchQuery({ query: sourceQuery }).subscribe(value =>
  client.writeData({ data: { resolvedDevice: doStuffWithTheData(value.data.selectedDevice) } });

const query = gql`
  query GetResolvedDevice {
    resolvedDevice @client
  }
`;

Because the field in the query passed to watchQuery lives in the cache, your handler will be called on every change, and we'll write the derived field to the cache in response. And because resolvedDevice now lives in the cache, the component that is querying for it will now get updates whenever it changes (which will be whenever the "upsteam" selectedDevice field changes).

Now you probably don't want to actually put that source field watch query at top level, as it will run and watch when your application starts whether you're using the rendering component or not. This would be especially bad if you adopt this approach for a bunch of local state fields. We're working on a method where you can declaratively define the derived fields and their fulfillment functions:

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

and then use an HOC to get them to kick in:

import { derivedFields } from './the-file-above';

export const withResolvedField = field => RenderingComponent => {
  return class ResolvedFieldWatcher extends Component {
    componentDidMount() {
      this.subscription = derivedFields[field].fulfill();
    }
    componentDidUnmount() {
      // I don't think this is actually how you unsubscribe, but there's
      // some way to do it
      this.subscription.cancel();
    }
    render() {
      return (
        <RenderingComponent {...this.props } />
      );
    }
  };
};

and finally wrap your modal container:

export default withDerivedField('resolvedDevice')(ModalContainer);

Note that I'm getting pretty hypothetical at the end here, I just typed it out instead of pulling our actual code down. We're also back on Apollo 2.5 and React 2.6, so you may have to adapt the approach for hooks, etc. The principle should be the same though: define derived fields by watching queries on source fields in the cache, and write the derived fields back to the cache. Then you have a reactive cascade from your source data to the component rendering ui based on the derived field.