JustGiving Logo

Migrating to Apollo Client 3 - Part 2: local state

29 January, 2021

Written by Phillip Parker

frontmatter

This post is part 2 in a four part series covering:

  • Updating your imports
  • Local state and reactive variables
  • Caching, keyFields and keyArgs
  • Pagination, modifying the cache and the merge function

Apollo Client 2 (AC2) allowed you to use writeData to modify the cache directly or local resolvers to query and mutate the cache using GraphQL. With the introduction of Apollo Client 3 (AC3), this has completely changed. writeData is no longer available and local resolvers are deprecated. Apollo now recommends using reactive variables to represent local state.

Setting up Reactive Variables

As a first example, I'll show you how to setup a boolean field representing if our user is currently logged in. We begin by defining a new reactive variable using makeVar. Apollo recommends setting this up in a new file called cache as it will also contain the initialisation logic of the InMemoryCache.

import { makeVar } from '@apollo/client';

export const isLoggedInVar = makeVar(false);

This reactive variable can be read and modified from anywhere in your application and doesn't enforce data normalisation, so it's now easier to store complex data types in your local state. If all we wanted to do was read and write to a variable anywhere in our component tree, this would be enough. We could use the useReactiveVar hook discussed below to re-render our components when the variable changes and make updates by calling the function returned by makeVar with our new value. However, if we wish to use the variable inside a GraphQL query, we first need to setup a field policy for it inside the cache.

In our cache file, we create a variable called cache, passing it an InMemoryCache that has a typePolicies field. The typePolicy object will define a policy for the Query type. This is our root type and will hold an object of fields that represent our local state and how the local state is resolved. By giving our field a read function we are able to resolve data in any way we like. This could be hardcoded, come from local storage, or retrieved from another service. In this case however, we want to use our reactive variable.

export const cache = new InMemoryCache({
  typePolicies: {
    Query: {
      fields: {
        isLoggedIn: {
          read() {
            return isLoggedInVar();
          },
        },
      },
    },
  },
});

Apollo's InMemoryCache accepts a typePolicies field. This new field is given a TypePolicy object that defines how the cache interacts with specific types in your schema. It accepts fields that correspond to your GraphQL types (both local and remote). For now, we're going to focus only on local types.

This new cache object can now be imported and used in our ApolloClient initialisation.

const client = new ApolloClient({ 
  cache,
})

Reading Reactive Variables

To use this state inside a React component we have two options. The first is to include it in a GraphQL query utilising the @client directive to let Apollo know we're requesting local state.

export const GET_IS_LOGGED_IN = gql`
  query GetIsLoggedIn {
    isLoggedIn @client
  }
`;

This can then be used inside our component using useQuery.

export function App() {
  const { data, loading, error } = useQuery(GET_IS_LOGGED_IN);

  if (loading) return <Loading />;
  if (error) return <p>ERROR: {error.message}</p>;
  if (!data.isLoggedIn) {
    return (
      <Redirect
        to={{
          pathname: '/public-page',
        }}
      />
    );
  }

  return (
    <div>
      <Header>My Private page</Header>
    </div>
  );
}

The second option is to use Apollo's new hook, useReactiveVar, to read directly from the variable.

import { useReactiveVar } from '@apollo/client';
import { isLoggedInVar } from './cache.ts'

export function App() {
  const isLoggedIn = useReactiveVar(isLoggedInVar);

  if (loading) return <Loading />;
  if (error) return <p>ERROR: {error.message}</p>;
  if (isLoggedIn) {
    return (
      <Redirect
        to={{
          pathname: '/public-page',
        }}
      />
    );
  }

  return (
    <div>
      <Header>My Private page</Header>
    </div>
  );
}

When to use a GraphQL query or useReactiveVar is up to you. If you need some quick and simple global state, useReactiveVar takes no time setup. However, if you'd like to keep some type safety, use @client in combination with something like graphql-code-generator.

Derived state

We can illustrate derived state using first and last name variables. If we have these two variables setup and our app requires the full name, we can derive that using the first and last name variables we have, instead of creating a new one.

export const cache = new InMemoryCache({
  typePolicies: {
    Query: {
      fields: {
        firstName: {
          read() {
            return firstNameVar()
          }
        },
        lastName: {
          read() {
            return lastNameVar()
          }
        },
        fullName: {
          read() {
            return `${firstNameVar()} ${lastNameVar()}`;
          },
        },
      },
    },
  },
});

As we don't have a reactive variable setup for the full name, to read this we have to add it to a GraphQL query and use @client.

export const GET_FULL_NAME = gql`
  query GetFullName {
    fullName @client
  }
`;

Modifying Reactive Variables

To modify our reactive variables we can call the function returned by makeVar, passing our new value. This will cause all components connected using @client or useReactiveVar to re-render, updating our UI.

isLoggedInVar(true)

As you can see, working with local state is now a lot easier. There's no more need for the complexity of local resolvers and you can even opt out of setting up a field policy, using the reactive variable directly with useReactiveVar. However, if you still require some type safety, integrating the reactive variable with a GraphQL query can easily be done using a field policy, and combined with graphql-code-generator, this just looks like another variable coming from your back-end service.