Let’s Talk About Executing and Caching Queries with React Apollo

Let’s Talk About Executing and Caching Queries with React Apollo

Yep, you’ve guessed it. We’re going to talk about queries.

Let’s start with basics. The useQuery React hook is the primary API for executing queries when using Apollo Client in React. To run a query within a React component, we call the useQuery and pass it a GraphQL query string. When the component renders, useQuery returns an object from Apollo Client that contains loading, error, and data properties.

const GET_DATA = gql`
  query GetData {
    items {
      id
      name
    }
  }
`;

const Data = () => {
    const { loading, error, data } = useQuery(GET_DATA);

    if (loading) return 'Loading...';
    if (error) return `Error! ${error.message}`;

    return (
        <div>
            {data.items.map((item) => (
                <div key={item.id}>{item.name}</div>
            ))}
        </div>
    );
}

Apollo Query component is a deprecated way of executing queries. It’s a part of @apollo/react-components package and it is implementing Render props pattern. Render props are used for sharing code between React components using a prop whose value is a function. A component with a render prop takes a function that returns a React element. The component then calls this function instead of implementing its own render logic. In the case of Query component, we are using the children prop as a render prop. But because the children prop doesn’t need to be named in the list of “attributes”, you can put it directly inside the element. In the following example, we are passing a query prop, which is required.

const Data = () => {
    <Query query={GET_DATA}>
        {(result) => {
            // render the query results
        }}
    </Query>
};

Internally, Query component actually uses the useQuery hook and then calls the render props function with results returned from the hook. This means that we’re using the same options and are getting the same result object as when using the useQuery hook.

Cache-first, Ask Questions Later

Apollo is locally caching results for the queries by default. This makes subsequent executions of the same query extremely fast. This is called cache-first policy. We can define fetch policy on query level.

const { loading, error, data } = useQuery(GET_DATA, {
  fetchPolicy: "cache-and-network" 
});

These are 6 supported fetch policies:

  • cache-first: This is the default fetch policy. If data is present in the cache, that data is returned. Otherwise, a query is executed against the GraphQL server and the data is returned after it is cached.

  • cache-only: Query is only executed against the cache. GraphQL server is never called.

  • cache-and-network: If data is present in the cache, that data is returned. But even then, a query is executed against the GraphQL server. If the response differs from what is stored in the cache, it will update the cache.

  • network-only: Query is executed against the GraphQL server, without first checking the cache. The query's result is stored in the cache in case the same query with a different fetch policy is being made elsewhere in the application.

  • no-cache: Behaves like network-only, except the query's result is not stored in the cache.

  • standby: Behaves like cache-first, except this query does not automatically update when underlying field values change. You have to update it manually.

There is also a possibility to define nextFetchPolicy option. This way, you can define fetch policy for the first query execution using fetchPolicy and then you can define fetch policy for the subsequent executions with nextFetchPolicy.

Apollo Client Behind the Scenes

Apollo Client acts as a facade to the underlying cached data. Behind the scenes, Apollo normalizes the data by splitting the results into individual objects and assigns a unique identifier to each object. These objects are stored in a flattened structure. Apollo then exposes an API to interact with the cached data. By minimizing direct access to the actual data using the facade/API, Apollo can normalize data under the hood.

Apollo can automatically update cache for:

  • Queries
  • Mutations that update a single existing entity
  • Bulk update mutations that return the entire set of changed items

Of course, there are use cases in which we have to manually update the cache:

  • Application-specific side-effects (something that happens after the mutation, but does not use data returned from the mutation)
  • Update operations that add, remove, or reorder items in a cached collection

Manually Updating Cached Data

Apollo supports multiple ways of reading and writing cached data:

  • readQuery / writeQuery
  • readFragment / writeFragment
  • cache.modify

With readQuery method, it is possible to run GraphQL queries directly on the local cache. If the cache contains all of the data necessary to execute a specified query, readQuery returns a data object in the shape of the query, just like a GraphQL server does. If some fields are missing from the cache, null is returned. Using writeQuery we can write arbitrary data to the cache for the specific query. It looks similar to readQuery, but it accepts data option.

const data = client.readQuery({query: GET_DATA});

const newItem = {
    id: '2',
    name: 'New item.',
    __typename: 'Item',
};

client.writeQuery({
    query,
    data: {
        messages: [...data.items, newItem],
    },
});

Using fragments it is possible to read or update only parts of the cached data, unlike readQuery / writeQuery methods, which require a complete query. When using fragments to interact with cache, we can use readFragment / writeFragment methods. They require id option, which represents the unique identifier that was assigned to the object in the cache. By default, this identifier has the format <_typename>:<id>, but this can be customized. If there is no object with the specified ID, readFragment returns null. writeFramgent accepts data option, which represents data that will be written for the specified object and that conforms to the specified fragment.

const itemId = 'Item:1';
const item = client.readFragment({
    id: itemId,
    fragment: gql`
        fragment ItemName on Item {
          name
        }
    `,
});
client.writeFragment({
    id: itemId,
    fragment: gql`
        fragment ItemName on Item {
          name
        }
    `,
    data: {
        name: `'${item.name}' was modified`,
        __typename: 'Item'
    }
});

With cache.modify it is possible to directly modify cached fields. This method requires the ID of the cached object to modify and a modifier function for each field to modify.

It is important to emphasize that any changes made with these methods are not pushed to the GraphQL server. If the current page is refreshed, these changes will disappear. Also, all methods trigger a refresh of all active queries that depend on modified fields.

Two Strategies for Updating the Cached Results

Besides manually rewriting cached data, there are two strategies for updating the cached results: polling and refetching.

With polling, we execute the query periodically at a specified interval.

const { loading, error, data } = useQuery(GET_DATA, {
    pollInterval: 500,
 });

Refetching is done by using refetch function that enables us to re-execute the query.

function Data() {
    const { loading, error, data, refetch } = useQuery(GET_DATA);

    if (loading) return 'Loading...';
    if (error) return `Error! ${error.message}`;

    return (
        <div>
            {data.items.map((item) => (
                <div key={item.id}>{item.name}</div>
            ))}
            <button onClick={() => refetch()}>Refetch!</button>
        </div>
    );
}

Apollo Client is a Powerful Caching Machine

In conclusion, one would have to agree that Apollo Client is a mighty caching machine. It’s equipped with a powerful caching mechanism that makes it easy to execute data queries quickly and efficiently. However, to make better use of its caching possibilities, one should get familiar with various methods of interacting with cache, cache setup and configuration.