w3resource

Local state management


In the previous tutorial, we have learnt how to manage remote data from our GraphQL server with Apollo Client, but what should we do with our local data? How can we be able to access boolean flags and device API results from multiple components in our app, but don't want to maintain a separate Redux or MobX store. Ideally, we would like the Apollo cache to be the single source of truth for all data in our client application.

Apollo Client has built-in local state handling capabilities that allow you to store your local data inside the Apollo cache alongside your remote data. To access your local data, just query it with GraphQL. You can even request local and server data within the same query!

In this tutorial, you'll learn how Apollo Client can help simplify local state management in your app. We'll cover how client-side resolvers can help us execute local queries and mutations. You'll also learn how to query and update the cache with the @client directive.

Updating local state

There are two main ways to perform local state mutations. The first way is to directly write to the cache by calling cache.writeData. Direct writes are great for one-off mutations that don't depend on the data that's currently in the cache, such as writing a single value. The second way is by leveraging the useMutation hook with a GraphQL mutation that calls a local client-side resolver. We recommend using resolvers if your mutation depends on existing values in the cache, such as adding an item to a list or toggling a boolean.

Direct writes

Direct writes to the cache do not require a GraphQL mutation or a resolver function. They leverage your Apollo Client instance directly by accessing the client property returned from the useApolloClient hook, made available in the useQuery hook result, or within the render prop function of the ApolloConsumer component. We recommend using this strategy for simple writes, such as writing a string, or one-off writes. It's important to note that direct writes are not implemented as GraphQL mutations under the hood, so you shouldn't include them in your schema. They also do not validate that the data you're writing to the cache is in the shape of valid GraphQL data. If either of these features are important to you, you should opt to use a local resolver instead.

import React from "react";
import { useApolloClient } from "@apollo/react-hooks";
import Link from "./Link";
function FilterLink({ filter, children }) {
  const client = useApolloClient();
  return (
    <Link onClick={() => client.writeData({ data: { visibilityFilter: filter } })} >
      {children}
  </Link>
  );
}

The ApolloConsumer render prop function is called with a single value, the Apollo Client instance. You can think of the ApolloConsumer component as being similar to the Consumer component from the React context API. From the client instance, you can directly call client.writeData and pass in the data you'd like to write to the cache.

What if we want to immediately subscribe to the data we just wrote to the cache? Let's create an active property on the link that marks the link's filter as active if it's the same as the current visibilityFilter in the cache. To immediately subscribe to a client-side mutation, we can use useQuery. The useQuery hook also makes the client instance available in its result object.

import React from "react";
import { useQuery } from "@apollo/react-hooks";
import gql from "graphql-tag";
import Link from "./Link";
const GET_VISIBILITY_FILTER = gql`
  {
    visibilityFilter @client
  }
`;
function FilterLink({ filter, children }) {
  const { data, client } = useQuery(GET_VISIBILITY_FILTER);
  return (
    <Link
      onClick={() => client.writeData({ data: { visibilityFilter: filter } })}
      active={data.visibilityFilter === filter}
    >
      {children}
    </Link>
  )
}

You'll notice in our query that we have a @client directive next to our visibilityFilter field. This tells Apollo Client to fetch the field data locally (either from the cache or using a local resolver), instead of sending it to our GraphQL server. Once you call client.writeData, the query result on the render prop function will automatically update. All cache writes and reads are synchronous, so you don't have to worry about loading state.

Local resolvers

If you'd like to implement your local state update as a GraphQL mutation, then you'll need to specify a function in your local resolver map. The resolver map is an object with resolver functions for each GraphQL object type. To visualize how this all lines up, it's useful to think of a GraphQL query or mutation as a tree of function calls for each field. These function calls resolve to data or another function call. So when a GraphQL query is run through Apollo Client, it looks for a way to essentially run functions for each field in the query. When it finds an @client directive on a field, it turns to its internal resolver map looking for a function it can run for that field.

To help make local resolvers more flexible, the signature of a resolver function is the exact same as resolver functions on the server built with Apollo Server. Let's recap the four parameters of a resolver function:

fieldName: (obj, args, context, info) => result;
  1. obj: The object containing the result returned from the resolver on the parent field or the ROOT_QUERY object in the case of a top-level query or mutation.
  2. args: An object containing all of the arguments passed into the field. For example, if you called a mutation with updateNetworkStatus(isConnected: true), the args object would be { isConnected: true }.
  3. context: An object of contextual information shared between your React components and your Apollo Client network stack. In addition to any custom context properties that may be present, local resolvers always receive the following:
    • context.client: The Apollo Client instance.
    • context.cache: The Apollo Cache instance, which can be used to manipulate the cache with context.cache.readQuery, .writeQuery, .readFragment, .writeFragment, and .writeData.
    • context.getCacheKey: Get a key from the cache using a __typename and id.
  4. info: Information about the execution state of the query. You will probably never have to use this one.

Let's take a look at an example of a resolver where we toggle a todo's completed status:

import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';

const client = new ApolloClient({
  cache: new InMemoryCache(),
  resolvers: {
    Mutation: {
      toggleTodo: (_root, variables, { cache, getCacheKey }) => {
        const id = getCacheKey({ __typename: 'TodoItem', id: variables.id })
        const fragment = gql`
          fragment completeTodo on TodoItem {
            completed
          }
        `;
        const todo = cache.readFragment({ fragment, id });
        const data = { ...todo, completed: !todo.completed };
        cache.writeData({ id, data });
        return null;
      },
    },
  },
});

In order to toggle the todo's completed status, we first need to query the cache to find out what the todo's current completed status is. We do this by reading a fragment from the cache with cache.readFragment. This function takes a fragment and an id, which corresponds to the todo item's cache key. We get the cache key by calling the getCacheKey that's on the context and passing in the item's __typename and id.

Once we read the fragment, we toggle the todo's completed status and write the updated data back to the cache. Since we don't plan on using the mutation's return result in our UI, we return null since all GraphQL types are nullable by default.

Let's learn how to trigger our toggleTodo mutation from our component:

import React from "react"
import { useMutation } from "@apollo/react-hooks";
import gql from "graphql-tag";

const TOGGLE_TODO = gql`
  mutation ToggleTodo($id: Int!) {
    toggleTodo(id: $id) @client
  }
`;

function Todo({ id, completed, text }) {
  const [toggleTodo] = useMutation(TOGGLE_TODO, { variables: { id } });
  return (
    <li
      onClick={toggleTodo}
      style={{
        textDecoration: completed ? "line-through" : "none",
      }}
    >
      {text}
    </li>
  );
}

First, we create a GraphQL mutation that takes the todo's id we want to toggle as its only argument. We indicate that this is a local mutation by marking the field with a @client directive. This will tell Apollo Client to call our local toggleTodo mutation resolver in order to resolve the field. Then, we create a component with useMutation just as we would for a remote mutation. Finally, pass in your GraphQL mutation to your component and trigger it from within the UI in your render prop function.

Previous: Mutations
Next: Subscriptions



Follow us on Facebook and Twitter for latest update.