Introduction to the Apollo local state and reactive variables
Marcin Kwiatkowski /
In one of my previous articles, I described the useReducer hook as an excellent way to manage the state of React apps, including apps connected with GraphQL APIs using the apollo client.
Typically in an app, you have a remote state (from sever, for API, here I mean from GraphQL API), but also you can have a local state that does not exist on the server side.
I said that useReducer is suitable for managing situations like that – moreover – using Apollo Client, there is another way to manage the local state.
Apollo Client for State Management
Apollo client 3 enables the management of a local state by incorporating field policies and reactive variables. Field Policy lets you specify what happens if you query specific fields, including those not specified for your GraphQL servers. Field policies define local fields so the field is populated with information stored anywhere, like local storage and reactive variables.
So Apollo Client (version >=3) provides two mechanisms to handle the local state:
- Local-only fields and field policies
- Reactive variables
What is the Apollo client?
The Apollo client connects React App and GraphQL API. Moreover, it is a state management library.
It helps you to connect your React App with GraphQL API. It provides methods to communicate with API, cache mechanisms, helpers, etc.
Besides, Apollo client provides an integrated state management system that allows you to manage the state of your whole application.
What is Apollo State?
Apollo Client has its state management system using GraphQL to communicate directly with external servers and provide scalability.
Apollo Client supports managing the local and remote state of applications, and you will be able to interact with any state on any device with the same API.
Local-only fields and field policies
This mechanism allows you to create your client schema. You can extend a server schema or add new fields.
Then, you can define field policies that describe wherefrom data came from. You can use Apollo cache or local storage.
The crucial advantage of this mechanism is using the same API as when you work with server schema.
Local state example
If you want to handle local data inside a standard GraphQL query, you have to use a @client directive for local fields:
1query getMissions ($limit: Int!){
2missions(limit: $limit) {
3 id
4 name
5 twitter
6 website
7 wikipedia
8 links @client // this field is local9 }
10}
11
Define local state using local-only fields
InMemory cache from Apollo
Apollo client provides a caching system for local data. Normalized data is saved in memory, and thanks to that, already cached data can get fast.
Field type policies
You can read and write to the Apollo client cache. Moreover, you can customize how a specific field in your cache is handled. You can specify read, write, and merge functions and add custom logic.
To define a local state, you need to:
1. Define field policy and pass it to the InMemoryCache
2. Add field to the query with @client directive
Local-only fields tutorial
Let's go deeper with the local-only field and check how they work in action.
1const client = new ApolloClient({
2uri: 'https://api.spacex.land/graphql/',
3cache: new InMemoryCache()
4});;
5
API.spacex.land/graphql is a fantastic free public demo of GraphQL API, so I use it here. If you want to explore that API, copy the URL to the browser: https://api.spacex.land/graphql/
Connect Apollo with React by wrapping the App component with ApolloProvider:
ApolloProvider takes the client argument, which is our already declared Apollo Client. We can use Apollo Client features in the App component and every child component, thanks to that.
The query for missions data
Let's get some data from the API. I want to get missions:
1query getMissions ($limit: Int!){
2missions(limit: $limit) {
3 id
4 name
5 twitter
6 website
7 wikipedia
8 }
9}
10
Results for this query when I passed 3 as a limit variable:
1import React from"react"23exportconst Missions = () => {
4return<div>5 Missions component should be here.
6</div>7}
89exportdefault Missions;
10
Now the test is passing
Let's re-export component in src/components/Missions/index.js
1export { default } from'./Missions';
2
We need to query for data using the useQuery hook provided by the Apollo client.
In unit tests, you need to have a component wrapped by ApolloProvider. For testing purposes, Apollo provides a unique Provider: MockedProvider, and it allows you to add some mock data. Let's use it.
Now, we can expect that three product missions are visible on the screen because, in our mock response, we have an array with three missions with corresponding names: 'Thaicom,' 'Telstar,' and 'Iridium NEXT.'
To do so, update the test case.
First, make the test case asynchronous by adding the async keyword before the it callback function.
Second, replace the getByText query with the findByText, which works asynchronously.
1 it('Should display name of each mission', async () => {
2const { findByText } = render(
3<MockedProvidermocks={mocks}>4<Missions/>5</MockedProvider>6 );
78await findByText('Thaicom');
9await findByText('Telstar');
10await findByText('Iridium NEXT');
11 });
12
The test fails because we don't query for the data in React component.
By the way, maybe, you think I don't wrap findBytext by the expect…toBe. I do not do that because the findByText query throws an error when it cannot find provided text as an argument, so I don't have to create an assertion because the test will fail if the text is not found.
Let's update the React component.
First import useQuery hook, and GET_MISSIONS query in src/components/Missions/Missions.js
Now, let's prepare content that Component will render for us. If missions exist, allow's display the name of each Mission. Otherwise, let's show the 'There is no missions' paragraph.
First, we must define the field policy for our local links field.
When you inspect docs for missions query in GraphQL API, you can see that it returns a Mission type:
So we need to add a links client field to the Mission type.
To do so, we need to add a configuration to InMeMoryCache in the src/index.js file like this:
1const client = new ApolloClient({
2uri: 'https://api.spacex.land/graphql/',
3cache: new InMemoryCache({
4typePolicies: {
5Mission: {
6fields: {
7links: {
8read(_, { readField }) {
9// logic will be added here in the next step10 }
11 }
12 }
13 }
14 }
15 })
16
Now let's return an array with links collected from the Mission. The read function has two arguments. The first one is the field's currently cached value if one exists. The second one is an object that provides several properties and helper functions. We will use the readField function to read other field data.
OK, you met local-only fields, and now let's look at another mechanism called: Reactive variables.
You can write and read data anywhere in your app using reactive variables.
Apollo client doesn't store reactive variables in its cache, so you don't have to keep the strict cached data structure.
Apollo client detects changes in reactive variables, and when the value changes, a variable is automatically updated in all places.
Reactive variables in action
This time I would like to show you the case using reactive variables. I don't want to repeat Apollo docs, so you can see the basics of reading and modifying reactive variables here.
The case
I've started work on the cart and mini-cart in my react-apollo-storefront app. The first thing that I needed to do was create an empty cart.
In Magento GraphQL API, there is the mutation createEmptyCart. That mutation returns the cart ID.
I wanted to get a cart ID, store it in my app, and after the page refresh, check if a value exists in the local state and, if yes, get it from it without running mutation.
Today I showed you two techniques for managing local data in Apollo. Local-only fields and reactive variables. Those mechanisms provide a lot of flexibility, and they should be considered when architecting state management in your React application. In addition, I recommend reading about mocking GraphQL queries and mutation.
Share this article with your friends!
Each share supports and motivates me to create new content. Thank you!
About the author
Marcin Kwiatkowski
Frontend developer, Certified Magento Full Stack Developer, writer, based in Poland. He has eight years of professional Software engineering experience. Privately, husband and father. He likes reading books, drawing, and walking through mountains.