How to use apollo client cache for local state management
Tags: graphql, apollo, react, state
Published: 2022-12-17 20:25:27

Apollo client is popular tool of choice for communicating with graphQL API. With its build-in cache capability it is also a good choice for state management. Let's take a look at how to perform CRUD actions on an example to-do app, using apollo client cache for local state management.  

This walkthrough uses TypeScript, react (18.2.0) and @apollo/client (3.7.1). User interface is purposefully basic to allow us to focus on interacting with the state. It is composed of four parts:  
- `AddTodo` a component with a text input element for adding to-dos  
- `TodoList` which displays all `TodoItem`s that will toggle completed state on click  
- `ClearCompleted` button that removes completed to-dos from cache    

Complete code is available on [codesandbox](https://codesandbox.io/s/mystifying-hodgkin-ybve5u)

## Initial config  
Let's start by defining our apollo client using `InMemoryCache`:  

```typescript
// client.ts
import { ApolloClient, InMemoryCache } from "@apollo/client";

export const client = new ApolloClient({
  cache: new InMemoryCache(),
});

```

And make it available to react via `ApolloProvider`:  
```tsx
// App.tsx
export default function App() {
  return (
    <ApolloProvider client={client}>
      <div className="App">
        <h1>Todo list</h1>
        <AddTodo />
        <TodoList />
        <br />
        <ClearCompleted />
      </div>
    </ApolloProvider>
  );
}

```  

## Setup initial state  
`useQuery` has default fetch policy set to `cache-first`. This policy means that apollo client will check the cache before attempting to send a network request to fetch the data. We want to use only a local state (in a cache) without using network, so initial state will need to be written into the cache before client requests the data.   
 
Going back to the `client.ts` file, write the initial data before the react app is mounted.
Call `writeQuery` on apollo client instance, defining the query to write into and the value of initial data.  

```ts
// client.ts
client.writeQuery({
  query: gql`
    query {
      allTodos
    }
  `,
  // initially an empty array
  data: { allTodos: [] }
});
```

Structure of the `data` property must conform the structure of the query. In the code above, query defining allTodos field expects `data` object containing property with the same name.   

> If the data property does not contain all the fields defined in the query, apollo will log the error in a console

This call creates canonical field `allTodos` on the root `Query` type. We can confirm this by opening [apollo devtools](https://github.com/apollographql/apollo-client-devtools) and viewing the 'cache' tab.  


![Apollo devtool showing allTodos field having a value of empty array](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k32gcvtszov7113s94uy.png)





## Type policies  
By default all query writes for `allTodos` will replace its content. Our goal is to use `allTodos` field for overwriting all stored to-dos and for adding individual to-dos into the cached array. The default write behaviour can be changed by defining the type policy on the root `Query` type, specifying `allTodos` field with the object containing the `merge` method.

```ts
export const client = new ApolloClient({
  cache: new InMemoryCache({
    typePolicies: {
      // type we want to define the policy for
      Query: {
        fields: {
          // field for which we want to customize the behaviour
          allTodos: {
            // how we want to merge existing data with the incoming
            merge(existing = [], incoming) {
              if (Array.isArray(incoming)) {
                return incoming;
              }

              return [...existing, incoming];
            }
          }
        }
      }
    }
  })
});
```  

Value returned by `merge` will be stored as a new value for `allTodos`. It will be invoked for all the query writes into `allTodos` field, even for the initial state write.

`merge` defined above returns the `incoming` value as-is if it is array, overwriting previously cached value which was our first goal for this field. In any other case `incoming` will be returned as a last item in an array of existing data.   
  
> Apollo suggests that merge function is 'pure' - not mutating the input arguments since internally the objects are passed as references and mutating them can cause unexpected side-effects.

 
## Adding a to-do item  
Adding a to-do is done similarly to setting the initial state. This time the query will define all the fields that will construct the to-do object.   
```tsx
// AddTodo.tsx
const todoQuery = gql`
  query {
    allTodos {
      __typename
      id
      title
      completed
    }
  }
`;
```  

Including the `__typename` is necessary for data normalization in apollo. If you are not familiar with this concept I recommend learning more about it from [this article](https://www.apollographql.com/blog/apollo-client/caching/demystifying-cache-normalization/). In short - it allows deduplication of data, enables recognition of correct type for using type policies, updating objects using `writeFragment` and other useful stuff.  

After our todoQuery is defined we will use it in a helper function which is invoked when you press "Add Todo" 
```tsx  
// AddTodo.tsx
function addTodo(client: ApolloClient, title: string) {
  client.writeQuery({
    query: todoQuery,
    data: {
      allTodos: {
        __typename: "Todo",
        id: crypto.randomUUID(),
        title,
        completed: false
      }
    }
  });
}
```   

As before it is necessary for the structure of the `data` to match the structure of the query. `__typename` is set to `Todo` which will be important later when we get to updating the `completed` state.  
You have probably noticed that for value of `allTodos` we have set a single object and not an array as before when writing the initial state. This is when the `merge` field policy on `allTodos` that was defined earlier comes in useful. Let's review it again:    

```tsx
// client.tsx
    merge(existing = [], incoming) {
      if (Array.isArray(incoming)) {
        return incoming;
      }
      return [...existing, incoming];
    }
```
`existing` is always the current value of the field and `incoming` will be set to the value of the to-do object we want to add using `writeQuery`. `incoming` fails the `isArray` test and is appended to the array of existing to-dos - in simpler terms: new to-do is added to the end of the list of existing to-dos.


## Query To-dos  
If you have used react apollo client, the following code will be familiar. Requesting data from cache is the same as requesting them from graphql server. First you define the query and then use it as a first argument in useQuery hook
```tsx
// TodoList.tsx
const allTodosQuery = gql`
  query getAllTodos {
    allTodos {
      id
      title
      completed
    }
  }
`;

export function TodoList() {
  const { data } = useQuery(allTodosQuery);
  // data: { allTodos: [{...}] }
  /* rest of the component body */
}
```  

The default fetch policy for `useQuery` (`cache-first`) means that cache will be queried for data and it returns the empty array that was setup as initial state.  

> If cache had no data for this query it will attempt to request if from the server. We can prevent this behaviour by [changing the fetchPolicy](https://www.apollographql.com/docs/react/data/queries/#setting-a-fetch-policy) to `cache-only`.  

Additionally to getting the data from cache, `useQuery` will set up subscription on the cached data. This means that whenever the value of `allTodos` is changed, component will be re-rendered with the latest cached value.   

Now that `TodoList` can get the data from cache it can render individual items as `TodoItem` components so now we can see the them in the UI.


## Updating to-do  
`TodoItem` takes to-do object as a prop and display its data. In this component we will want to add a functionality of toggling the `completed` state when user clicks on it. We will use `writeFragment` method to-do the update, which will require a fragment on a type that we want to edit:  

```tsx  
// TodoItem.tsx
const fragment = gql`
  fragment ToggleComplete on Todo {
    completed
  }
`;
```  

> This code creates a fragment on `Todo` type, which matches the value of `__typename` we have used when adding to-do.  

In a fragment we need to specify the properties that we will want to change, in this case it is just `completed` field. As a next step we will define `toggleComplete` helper that we will call when user clicks on the to-do:
 
```tsx
// TodoItem.tsx
function toggleComplete(client, todo) {
  client.writeFragment({
    id: client.cache.identify({ __typename: "Todo", ...todo }),
    fragment,
    data: {
      completed: !todo.completed
    }
  });
}
```
Notice the `client.cache.identify({ __typename: "Todo", ...todo })` utility used that reads the object `id` and `__typename`. The output will look something like: `Todo:123`. This is the 'ref' of the normalized object stored in a cache which tells the cache what object will be updated.    

Now when you click the to-do item and `toggleComplete` is called, it will update the `completed` property of the to-do object in cache. Cache change in turn triggers subscription set by `useQuery` in `TodoList` causing it to re-render with the new data that are being passed as a props into `TodoItem`. In the UI you will see to-do item that was clicked get crossed out.

## Deleting Todos  
To complete the CRUD implementation we will add delete functionality of the completed to-dos.  

In order to delete completed to-dos we need to get the list of to-dos, filter out the completed ones and write the new list back. All of this can be done with `updateQuery` method, so let's start by defining the query:  


```tsx  
// ClearCompleted.tsx
const queryTodos = gql`
  query Todos {
    allTodos {
      id
      completed
    }
  }
`;
```  

The update needs `id` so cache can recognize what to-dos are written back and `completed` to let us filter out completed to-dos. 

Update helper will look like this:  
```tsx
function clearCompletedTodos(client: ApolloClient) {
  client.cache.updateQuery(
    { query: queryTodos },
    (data) => {
      return { allTodos: data.allTodos.filter(({ completed }) => completed === false) };
    }
  );
  client.cache.gc();
}
```

This time the function signature is a little different. In a first parameter query must be defined in an object and the second parameter is update function. It receives `data` from the query in a shape as requested and it is expected to return the data we would like to have stored on that query. Code above returns object with `allTodos` property (again matching the shape of the query) with filtered to-dos. This update, same as previous writes, will invoke the `merge` field policy and because we are writing an array it replaces all of its content.  

> If you're wondering how are the titles of the to-dos preserved when it is not requested in the update query, it is because of the data normalization that happens internally. The to-do objects that are written back to the query contain `id` and `__typename` - which is requested implicitly, and those two properties are enough to match the object to normalized version stored in cache.  

After the update query, [cache garbage collection](https://www.apollographql.com/docs/react/caching/garbage-collection/) is invoked to clean up now unreachable normalized objects.  

> This currently doesn't work as expected so I have logged this [issue](https://github.com/apollographql/apollo-client/issues/10307) in apollo client repo.  

## Summary  
We've covered the cache policies and when they are invoked, setting up the initial state, writing objects into an array and the importance of `__typename`, querying the data from cache, using fragment to update the normalized object and finally deleting the completed to-dos.  

Thank you for reading this article, I hope you found it helpful. When you have a minute to spare I would love to hear your feedback on the information in the article, whether you have learned something new or any suggestions how to improve in my future writing.