React Apollo: Understanding Fetch Policy with useQuery
Tags: react, apollo, graphql, webdev
Published: 2022-01-29 18:32:50

I've been working on an Apollo client project these days. I wasn't used to GraphQL so I struggled to understand it at the beginning. <br />
In the app I'm working on, there was something wrong with the data consistency at some point. <br />
I googled for that and I got to know that apollo client uses `cache-first` as a default fetch policy. I changed the fetch policy in the project to `no-cache` because I thought that would fit more to the project. After that I found some bugs with `no-cache` and I felt that something went wrong. I thought it might be good to learn more about fetch policy.

I'm going to talk about

* Fetch Policy with useQuery
* Changing Default Fetch Policy

I prepared a simple todo graphql server using nest. There is no database. The server utilizes just an array as a storage and I'm going to use this server for the following tests. <br />
You can check the backend server code from [this repository](https://github.com/hsk-kr/graph-ql-test-server). <br />
I set up `"@apollo/client": "3.5.8"` in the client.

## Fetch Policy with `useQuery`

There are six fetch policies that are available on `useQuery`.

|NAME|	DESCRIPTION|
|---|---|
|cache-first|Apollo Client first executes the query against the cache. If all requested data is present in the cache, that data is returned. Otherwise, Apollo Client executes the query against your GraphQL server and returns that data after caching it. Prioritizes minimizing the number of network requests sent by your application. This is the default fetch policy. |
|cache-only|Apollo Client executes the query only against the cache. It never queries your server in this case. A cache-only query throws an error if the cache does not contain data for all requested fields.|
|cache-and-network| Apollo Client executes the full query against both the cache and your GraphQL server. The query automatically updates if the result of the server-side query modifies cached fields.  Provides a fast response while also helping to keep cached data consistent with server data.|
| network-only | Apollo Client executes the full query against your GraphQL server, without first checking the cache. The query's result is stored in the cache. Prioritizes consistency with server data, but can't provide a near-instantaneous response when cached data is available.|
|no-cache| Similar to network-only, except the query's result is not stored in the cache.|
|standby | Uses the same logic as cache-first, except this query does not automatically update when underlying field values change. You can still manually update this query with refetch and updateQueries.|

Source: [Apollo Documentation](https://www.apollographql.com/docs/react/data/queries/)

I'll show you how each fetch policy works.

### cache-first

This is a default fetch policy which uses cache if there is data in the cache, otherwise it fetches data from the server. <br />
I wrote a code for this test. There are two buttons. One is used for creating a todo item and another one is used for showing or hiding a data table (`mount` and `unmount`). The data table gets data with `useQuery`.<br />

Here is the code.

```javascript
import { useCallback, useState } from "react";
import {
  ApolloClient,
  InMemoryCache,
  ApolloProvider,
  useQuery,
  useMutation,
  gql,
} from "@apollo/client";

let suffixIndex = 1;

const GET_TODOS = gql`
  query {
    getTodos {
      id
      content
      checked
    }
  }
`;

const CREATE_TODO = gql`
  mutation CreateTodo($content: String!) {
    ct1: createTodo(content: $content) {
      id
      content
      checked
    }
  }
`;

const client = new ApolloClient({
  uri: "http://localhost:3000/graphql",
  cache: new InMemoryCache(),
});

function TodosTable() {
  const { data: todosData, loading: todosLoading } = useQuery(GET_TODOS);

  if (todosLoading) return <span>Loading...</span>;

  return (
    <table>
      <thead>
        <tr>
          <th>id</th>
          <th>content</th>
          <th>checked</th>
        </tr>
      </thead>
      <tbody>
        {todosData?.getTodos.map((todo) => (
          <tr key={todo.id}>
            <td>{todo.id}</td>
            <td>{todo.content}</td>
            <td>{todo.checked}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}

function App() {
  const [createTodo] = useMutation(CREATE_TODO);
  const [todosTableVisible, setTodosTableVisible] = useState(false);

  const handleCreateButtonClick = useCallback(() => {
    createTodo({
      variables: {
        content: `Item ${suffixIndex + 1}`,
      },
    });
  }, [createTodo]);

  const toggleTodosTableVisible = useCallback(() => {
    setTodosTableVisible((prevState) => !prevState);
  }, []);

  return (
    <div>
      <button type="button" onClick={handleCreateButtonClick}>
        Create Todo Item
      </button>
      <button type="button" onClick={toggleTodosTableVisible}>
        Toggle TodosTable Visible
      </button>
      {todosTableVisible && <TodosTable />}
    </div>
  );
}

const Provider = () => (
  <ApolloProvider client={client}>
    <App />
  </ApolloProvider>
);

export default Provider;
```


Let's see how it works step by step.

1\. Press the toggle button

![There is an empty table](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g1g4j5j6b1396ga6lygl.png) 

2\. Press the create button twice

![Request headers](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tus8kyu1al54yrqgrtyp.png)
 
![A request payload](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k1ldy96s02k00zf7f010.png)
 
![The request response](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2rc4mj50c4862o13k8af.png)

You can see the created data in a network tab.

3\. Press the toggle button twice (For remounting the component)

![A still empty table](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3dd74p8gb5634rq2ds07.png)

![A same network](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/30yjdzu7dbo21iqw2d2z.png)

There is still the empty table, right? There are even no additional requests in the network tab.

4\. Reload the tab and toggle the table

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/48b81gjjq4xp8qf32vjf.png)

Now, you can see the table. Let me explain it.

At the first request, the client got an empty array from the server and it stored the data in the cache. <br />
I remounted the table (step 3) and it found the empty array in the cache which is why the table was still empty.<br />

After reloading, they display the data from the server because the cache is gone.

### cache-only

It only uses cache. If there is no cached data, it throws an error.<br />
I rewrote the code for testing this option.

```javascript
function TodosTable() {
  const {
    data: todosData,
    loading: todosLoading,
    error,
  } = useQuery(GET_TODOS, {
    fetchPolicy: "cache-only",
  });

  if (todosLoading) return <span>Loading...</span>;

  console.log({ todosData, todosLoading, error });
  if (error) {
    return <h1>Error: {error}</h1>;
  }

  return (
    <table>
      <thead>
        <tr>
          <th>id</th>
          <th>content</th>
          <th>checked</th>
        </tr>
      </thead>
      <tbody>
        {todosData?.getTodos.map((todo) => (
          <tr key={todo.id}>
            <td>{todo.id}</td>
            <td>{todo.content}</td>
            <td>{todo.checked}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}

function App() {
  const [fetchTodos] = useLazyQuery(GET_TODOS);
  const [createTodo] = useMutation(CREATE_TODO);
  const [todosTableVisible, setTodosTableVisible] = useState(false);

  const handleFetchTodos = useCallback(() => {
    fetchTodos();
  }, [fetchTodos]);

  const handleCreateButtonClick = useCallback(() => {
    createTodo({
      variables: {
        content: `Item ${suffixIndex + 1}`,
      },
    });
  }, [createTodo]);

  const toggleTodosTableVisible = useCallback(() => {
    setTodosTableVisible((prevState) => !prevState);
  }, []);

  return (
    <div>
      <button type="button" onClick={handleFetchTodos}>
        Fetch Todos
      </button>
      <button type="button" onClick={handleCreateButtonClick}>
        Create Todo Item
      </button>
      <button type="button" onClick={toggleTodosTableVisible}>
        Toggle TodosTable Visible
      </button>
      {todosTableVisible && <TodosTable />}
    </div>
  );
}
```


1\. Press the toggle button

![An empty table](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/py401zwyr4ixokefl232.png)

To be honest, I didn't expect this result. I thought it would throw an error since they say `A cache-only query throws an error if the cache does not contain data for all requested fields.` in the documentation. Anyways, let's continue.

2\. Reload and press the fetch button.

![A result of fetching data](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v5m7wobautmyv3rxwldu.png)

You can see the response data in the network tab.

3\. Press the toggle button.

![Data Table](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l92ey60zyk1tj1iv53qe.png)

Now, you can see the data.

4\. Press the create button then remount(press the toggle button twice) the table

![Same Data](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zsvw9eiz0h4enyng6hnj.png)

It is still the same. `cache-only` uses only cached data as you have seen. 


If you fetch the data manually, it will show up as well but what if you fetch a part of the data? How will it show up? <br/>
Let's see how it appears.

```javascript
const GET_TODOS2 = gql`
  query {
    getTodos {
      id
      checked
    }
  }
`;

const [fetchTodos] = useLazyQuery(GET_TODOS2);
```

![Partially updated](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gmkyn1e5stc3h40qv4nr.png)
 
The data appears depending on which data is in the cache.

---

*Sorry, I didn't notice that there were empty columns and all of the numbers were 2. I changed a part of the code from*

```javascript
<td>{todo.checked}</td>

...

const handleCreateButtonClick = useCallback(() => {
    createTodo({
      variables: {
        content: `Item ${suffixIndex + 1}`,
      },
    });
  }, [createTodo]);
```

To

```javascript
<td>{todo.checked ? "checked" : "unchecked"}</td>

...

const handleCreateButtonClick = useCallback(() => {
    createTodo({
      variables: {
        content: `Item ${suffixIndex}`,
      },
    });
    suffixIndex++;
  }, [createTodo]);
```

---

### cache-and-network

With this policy, it first uses a data from the cache and makes a request. The request automatically updates the data.

For this test, I removed a code that renders a loading text in TodosTable.

```javascript
function TodosTable() {
  const {
    data: todosData,
    error,
  } = useQuery(GET_TODOS, {
    fetchPolicy: "cache-and-network",
  });

  if (error) {
    return <h1>Error: {error}</h1>;
  }

  return (
    <table>
      <thead>
        <tr>
          <th>id</th>
          <th>content</th>
          <th>checked</th>
        </tr>
      </thead>
      <tbody>
        {todosData?.getTodos.map((todo) => (
          <tr key={todo.id}>
            <td>{todo.id}</td>
            <td>{todo.content}</td>
            <td>{todo.checked ? "checked" : "unchecked"}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}
```

While loading, the component would use a data from a cache.<br />
Since we're living in the future with our internet speed, we won't be able to recognize. So let's slow down the internet to 3G first and then start the test.<br />

1\. Create two items and press the toggle button

![created two items](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/19g96k22a0jpc9984y7j.png)
  
2\. Create two items and remount the table

![updates automatically](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vue1mwzbc5qupy08xigk.gif)

It displays data out of the box from the cache, then it updates automatically when the fetching is done. 

### network-only

This uses the data comming from the server and then it updates the cache.

1\. Press the toggle button repeatedly


![delay of network only](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uxj5b9gymgdw52979ze3.gif)

It has a delay until a request comes back. 

For the next test, whether `network-only` updates the cache or not, I changed my code as below.

```javascript
function TodosTable() {
  const { data: todosData, error } = useQuery(GET_TODOS, {
    fetchPolicy: "cache-only",
  });

  if (error) {
    return <h1>Error: {error}</h1>;
  }

  return (
    <table>
      <thead>
        <tr>
          <th>id</th>
          <th>content</th>
          <th>checked</th>
        </tr>
      </thead>
      <tbody>
        {todosData?.getTodos.map((todo) => (
          <tr key={todo.id}>
            <td>{todo.id}</td>
            <td>{todo.content}</td>
            <td>{todo.checked ? "checked" : "unchecked"}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}

function App() {
  const [fetchTodos] = useLazyQuery(GET_TODOS, {
    fetchPolicy: "network-only",
  });
  const [createTodo] = useMutation(CREATE_TODO);
  const [todosTableVisible, setTodosTableVisible] = useState(false);

  const handleFetchTodos = useCallback(() => {
    fetchTodos();
  }, [fetchTodos]);

  const handleCreateButtonClick = useCallback(() => {
    createTodo({
      variables: {
        content: `Item ${suffixIndex}`,
      },
    });
    suffixIndex++;
  }, [createTodo]);

  const toggleTodosTableVisible = useCallback(() => {
    setTodosTableVisible((prevState) => !prevState);
  }, []);

  return (
    <div>
      <button type="button" onClick={handleFetchTodos}>
        Fetch Todos
      </button>
      <button type="button" onClick={handleCreateButtonClick}>
        Create Todo Item
      </button>
      <button type="button" onClick={toggleTodosTableVisible}>
        Toggle TodosTable Visible
      </button>
      {todosTableVisible && <TodosTable />}
    </div>
  );
}
```

1\. Press the fetch button then press the toggle button

![it display data well](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2d4y2bk421qco1hulwbh.png)
 
The table displays the data with `cache-only`. It means that `network-only` updated the cache.


### no-cache

It's similar to `network-only` but it doesn't update the cache. In the code above, I changed a line which is an option of the lazy query.

```javascript
 const [fetchTodos] = useLazyQuery(GET_TODOS, {
    fetchPolicy: "no-cache",
  });
```

1. Press the fetch button then press the toggle button

![Empty Data](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ezvu0ynjoai1bb210890.png)
 
Nothing shows up in the table with `cache-only` because `no-cache` doesn't update the cache.

## Changing Default Fetch Policy

As I already mentioned, a default option of `useQuery` and `useLazyQuery` is `cache-first`. If you want to change a default fetch policy, use [defaultOptions](https://www.apollographql.com/docs/react/api/core/ApolloClient/#example-defaultoptions-object).

```javascript
const client = new ApolloClient({
  uri: "http://localhost:3000/graphql",
  cache: new InMemoryCache(),
  defaultOptions: {
    watchQuery: {
      fetchPolicy: "cache-only",
      errorPolicy: "ignore",
    },
    query: {
      fetchPolicy: "network-only",
      errorPolicy: "all",
    },
    mutate: {
      errorPolicy: "all",
    },
  },
});
```

## Conclusion

There were a lot of things that I had to know more about Apollo Client. I didn't understand why they used `cache` as a default. That's why I set a default fetch policy of my project to `no-cache`. However, I got some problems while using `no-cache`. One of them is that `useQuery` doesn't use the `defaultOptions`. Although the problem was solved in a [commit](https://github.com/apollographql/apollo-client/commit/fcaa208c1b657079a002197a328b8e912c85ec60), it seems that there were some more issues related to `no-cache`. I thought it will be okay to use a specific policy when it is needed but apollo cache system does something more than I expected (like `automatically updating and making a rendering`, `refetchQueries`). I think `cache` may be the key to using apollo client but I'll have to learn more about it. I hope this post will help you at some point. Thank you for reading the post.