This post is part 2 in a four part series covering:
- Updating your imports
- Local state and reactive variables
- Caching,
keyFields
andkeyArgs
- 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.