Introduction to the Apollo local state and reactive variables

Marcin Kwiatkowski /
Introduction to the Apollo local state and reactive variables

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.

Introduction to the Apollo local state and reactive variables 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 talked that useReducer is suitable to manage situations like that – moreover – using Apollo Client, there is another way to manage local state.  Apollo Client for State Management Apollo client 3 enables the management of a local state through 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 that the field is populated with information stored anywhere, like local storage and reactive variables.  So  Apollo Client (version >=3) provides two mechanisms to handle local state:  Local-only fields and field policies Reactive variables What is the Apollo client? The Apollo client connects React App, GraphQL API, and besides 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.  graphql client  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:  query getMissions ($limit: Int!){     missions(limit: $limit) {         id         name         twitter         website         wikipedia         links @client // this field is local     } }   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 polices You can read and write to 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 there.   To define a local state, you need to:  Define field policy and pass it to the InMemoryCache 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.  Initialize project using Create React App  npx create-react-app local-only-fields Install apollo client npm install @apollo/client graphql    Initialize Apollo client Import apollo client stuff in index.js:   import {  ApolloClient,  InMemoryCache,  ApolloProvider, } from "@apollo/client"; Create client instance  const client = new ApolloClient({  uri: 'https://api.spacex.land/graphql/',  cache: new InMemoryCache() });; 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 App component with ApolloProvider:   <ApolloProvider client={client}>      <App /> </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:  query getMissions ($limit: Int!){   missions(limit: $limit) {     id     name     twitter     website     wikipedia   } } Results for this query when I passed 3 as a limit variable:   {   "data": {     "missions": [       {         "id": "9D1B7E0",         "name": "Thaicom",         "twitter": "https://twitter.com/thaicomplc",         "website": "http://www.thaicom.net/en/satellites/overview",         "wikipedia": "https://en.wikipedia.org/wiki/Thaicom"       },       {         "id": "F4F83DE",         "name": "Telstar",         "twitter": null,         "website": "https://www.telesat.com/",         "wikipedia": "https://en.wikipedia.org/wiki/Telesat"       },       {         "id": "F3364BF",         "name": "Iridium NEXT",         "twitter": "https://twitter.com/IridiumBoss?lang=en",         "website": "https://www.iridiumnext.com/",         "wikipedia": "https://en.wikipedia.org/wiki/Iridium_satellite_constellation"       }     ]   } } Let's create a React component that receives that data and, for now, displays the name of the Mission on the screen.  First, create a unit test: src/components/Missions/__tests__/Missions.spec.js  import { render } from "@testing-library/react" import Missions from '../Missions';   describe('Missions component', () => {    it('Should display name of each mission', () => {        const { getByText } = render(<Missions/>);          getByText('Missions component should be here.')      }) }) Of course, the test fails because we event doesn't have a component created yet.   test case of the above example  Add Component: src/components/Missions/Missions.js  import React from "react"   export const Missions = () => {    return <div>        Missions component should be here.    </div> }   export default Missions; Now the test is passing  react components tests   Let's re-export component in src/components/Missions/index.js  export { default } from './Missions'; 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.   // src/components/Missions/__tests__/Missions.spec.js  Import MockedProvider:   import { MockedProvider } from '@apollo/client/testing'; Define mocks:   const mocks = [    {        request: {            query: GET_MISSIONS,            variables: {                limit: 3,            },        },        result: {            "data": {                "missions": [                {                    "id": "9D1B7E0",                    "name": "Thaicom",                    "twitter": "https://twitter.com/thaicomplc",                    "website": "http://www.thaicom.net/en/satellites/overview",                    "wikipedia": "https://en.wikipedia.org/wiki/Thaicom"                },                {                    "id": "F4F83DE",                    "name": "Telstar",                    "twitter": null,                    "website": "https://www.telesat.com/",                    "wikipedia": "https://en.wikipedia.org/wiki/Telesat"                },                {                    "id": "F3364BF",                    "name": "Iridium NEXT",                    "twitter": "https://twitter.com/IridiumBoss?lang=en",                    "website": "https://www.iridiumnext.com/",                    "wikipedia": "https://en.wikipedia.org/wiki/Iridium_satellite_constellation"                }                ]            }        }    }, ]; The test fails because we don't have the GET_MISSIONS query defined yet. client side query  Create the file queries/missions.gql.js with the following content:  import { gql } from '@apollo/client';   export const GET_MISSIONS = gql`    query getMissions ($limit: Int!){        missions(limit: $limit) {            id            name            twitter            website            wikipedia        }    }  `;   Import query in the src/components/Missions/__tests__/Missions.spec.js  import { GET_MISSIONS } from "../../../queries/missions.gql"; Now let's wrap the Missions component by the Mocked provider.   const { getByText } = render(    <MockedProvider mocks={mocks}>       <Missions/>    </MockedProvider> ); 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.     it('Should display name of each mission', async () => {        const { findByText } = render(            <MockedProvider mocks={mocks}>                <Missions/>            </MockedProvider>        );          await findByText('Thaicom');        await findByText('Telstar');        await findByText('Iridium NEXT');    }); 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.   data returned does not have specified text  Let's update the React component.   First import useQuery hook, and GET_MISSIONS query in src/components/Missions/Missions.js  import { useQuery } from "@apollo/client"; import { GET_MISSIONS } from "../../queries/missions.gql"; Let's query for the data in the component body:  const  { data } = useQuery(GET_MISSIONS, {        variables: {            limit: 3        } }); 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.  const shouldDisplayMissions = useMemo(() => {     if (data?.missions?.length) {         return data.missions.map(mission => {             return <div key={mission.id}>                 <h2>{mission.name}</h2>             </div>         })     }       return <h2>There are no missions</h2> }, [data]);   In the end, the Component needs to return shouldDisplayMissions:  return shouldDisplayMissions; The full component code:  import React, { useMemo } from "react" import { useQuery } from "@apollo/client"; import { GET_MISSIONS } from "../../queries/missions.gql";   export const Missions = () => {    const  { data } = useQuery(GET_MISSIONS, {        variables: {            limit: 3        }    });      const shouldDisplayMissions = useMemo(() => {        if (data?.missions?.length) {            return data.missions.map(mission => {                return <div key={mission.id}>                    <h2>{mission.name}</h2>                </div>            })        }          return <h2>There are no missions</h2>    }, [data]);      return shouldDisplayMissions; }   export default Missions; Now, the test passed!  state management - final test caseThe last thing for this step is to inject components into the app and see missions in the browser.   // App.js  import Missions from './components/Missions';   function App() {  return <Missions/>; }   export default App;   It works but, initially, it shows, "There are no missions." Let fix it by adding a loading indicator in Missions.js.   First, grab the loading flag from the useQuery hook results:   const  { data, loading } = useQuery(GET_MISSIONS, {     variables: {         limit: 3     } }); Add loading indicator:   if (loading) {     return <p>Loading...</p> }  return shouldDisplayMissions; Besides, add a little bit of styling.   //src/components/Missions/Missions.module.css .mission {    border-bottom: 1px solid black;    padding: 15px; }   Now, import CSS module in Missions.js file:   import classes from './Missions.module.css' and add mission class to the mission div:  return <div key={mission.id} className={classes.mission}>     <h2>{mission.name}</h2> </div>  Here is the result:   local state management - example  Add local-only field OK, so we have data from API. The next task is to display links for the Mission. API returns three fields:   twitter website Wikipedia We can create our local field called: links. It will be an array with links, so we will be able to loop through that array and just display links.   First, let's add a new test case:  it ('Should display links for the mission', async () => {     const localMocks = [         {             ...mocks[0],             result: {                 data: {                     missions: [                         {                             "id": "F4F83DE",                             "name": "Telstar",                             "links": ['https://www.telesat.com/']                         },                     ]                 }             }         }     ]     const { findByText } = render(         <MockedProvider mocks={localMocks}>             <Missions/>         </MockedProvider>     );       await findByText('https://www.telesat.com/"') }); So, we expect that there will be rendered one link: "https://www.telesat.com/"  Define field policy 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:  graphql query   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:   const client = new ApolloClient({  uri: 'https://api.spacex.land/graphql/',  cache: new InMemoryCache({    typePolicies: {      Mission: {        fields: {          links: {            read(_, { readField }) {              // logic will be added here in the next step            }          }        }      }    }  }) 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.   Our logic for the links local field:  read(_, { readField }) {     const twitter = readField('twiiter');     const wikipedia = readField('wikipedia');     const website = readField('website');     const links = [];       if (twitter) {       links.push(twitter);     }       if (wikipedia) {       links.push(wikipedia);     }       if (website) {       links.push(website);     }       return links;   } The query for local-only field The next step is to include the links field in the query. Let's modify the GET_MISSIONS query:  query getMissions ($limit: Int!){     missions(limit: $limit) {         id         name         twitter         website         wikipedia         links @client     } }  You can define the local-only field by adding the @client directive after the field name.   Display local-only field on the screen We have made good progress, but the test still fails because the Component does not render any links yet.apollo - managing local state - test case  Please update the Missions component by modifying the shouldDisplayMissions Memo function.  const shouldDisplayMissions = useMemo(() => {     if (data?.missions?.length) {         return data.missions.map(mission => {             const shouldDisplayLinks = mission.links?.length ? mission.links.map(link => {                 return <li key={`${mission.id}-${link}`}>                     <a href={link}>{link}</a>                 </li>             }) : null;               return <div key={mission.id} className={classes.mission}>                 <h2>{mission.name}</h2>                 {shouldDisplayLinks}             </div>         })     }       return <h2>There are no missions</h2> }, [data]); We are good now. Everything work as well in the browser:  local and server data in action  And tests pass:  managing local state - all tests pass  Working Demo Here you can see the demo of the app:  https://apollo-client-local-only-fields-tutorial.vercel.app/  Source code Here you can find the source code for this tutorial: https://github.com/Frodigo/apollo-client-local-only-fields-tutorial  Here are the commits for each step:  Initialize project using Create React App Install Apollo Client Initialize Apollo Client The query for missions data Add local-only field   Reactive variables OK, you met local-only fields, and now let's take a 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 structure of cached data.  Apollo client detects changes of 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 of 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.  apollo local state management - example case to use  Implementation First, let's define the reactive variable:   import { makeVar } from "@apollo/client";  export const CART_ID_IDENTIFER = 'currentCardId' export const cartId = makeVar(localStorage.getItem(CART_ID_IDENTIFER)); Second, use that variable in a component, context, or hook and make it reactive:  import { useReactiveVar } from '@apollo/client';  export const CartProvider = ({ children }) => {     const currentCartId = useReactiveVar(cartId); }; Third, define the mutation to collect a cart Id from the server:  // mutation:  import { gql } from '@apollo/client';  export const CREATE_EMPTY_CART = gql`     mutation createEmptyCart {         createEmptyCart     } `;  // component // imports import { useMutation } from '@apollo/client'; import { CREATE_EMPTY_CART } from '../../mutations/cart.gql'  // ... in component body const [ createEmptyCart ] = useMutation(CREATE_EMPTY_CART, {     update(_, { data: { createEmptyCart } }) {         cartId(createEmptyCart);         localStorage.setItem(CART_ID_IDENTIFER, createEmptyCart);         } }); I used the update callback there, and I updated the reactive variable:  cartId(createEmptyCart) Then I also updated a value in the local storage.  Last, check if cart ID exists in the local state and if not, send the mutation to a sever.  const currentCartId = useReactiveVar(cartId);  useEffect(() => {     if (!currentCartId) {         createEmptyCart();     } }, [createEmptyCart, currentCartId]); Summary Today I showed you two techniques of managing local data in Apollo. Local-only fields, and reactive variables. Those mechanism provides a lot of flexibility, and and they should be consider when you architecting state management in your React 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!){
2    missions(limit: $limit) {
3        id
4        name
5        twitter
6        website
7        wikipedia
8        links @client // this field is local
9    }
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. 1. Define field policy and pass it to the InMemoryCache

  2. 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.

Initialize the project using Create React App

1 npx create-react-app local-only-fields
2

Install apollo client

1npm install @apollo/client graphql   
2

Initialize Apollo client

Import apollo client stuff in index.js: 

1import {
2 ApolloClient,
3 InMemoryCache,
4 ApolloProvider,
5} from "@apollo/client";
6

Create client instance

1const client = new ApolloClient({
2 uri: 'https://api.spacex.land/graphql/',
3 cache: 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: 

1<ApolloProvider client={client}>
2     <App />
3</ApolloProvider>
4

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!){
2  missions(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: 

1{
2  "data": {
3    "missions": [
4      {
5        "id": "9D1B7E0",
6        "name": "Thaicom",
7        "twitter": "https://twitter.com/thaicomplc",
8        "website": "http://www.thaicom.net/en/satellites/overview",
9        "wikipedia": "https://en.wikipedia.org/wiki/Thaicom"
10      },
11      {
12        "id": "F4F83DE",
13        "name": "Telstar",
14        "twitter": null,
15        "website": "https://www.telesat.com/",
16        "wikipedia": "https://en.wikipedia.org/wiki/Telesat"
17      },
18      {
19        "id": "F3364BF",
20        "name": "Iridium NEXT",
21        "twitter": "https://twitter.com/IridiumBoss?lang=en",
22        "website": "https://www.iridiumnext.com/",
23        "wikipedia": "https://en.wikipedia.org/wiki/Iridium_satellite_constellation"
24      }
25    ]
26  }
27}
28

Let's create a React component that receives that data and, for now, displays the name of the Mission on the screen.

First, create a unit test: src/components/Missions/__tests__/Missions.spec.js

1import { render } from "@testing-library/react"
2import Missions from '../Missions';
3 
4describe('Missions component', () => {
5   it('Should display name of each mission', () => {
6       const { getByText } = render(<Missions/>);
7 
8       getByText('Missions component should be here.')
9 
10   })
11})
12

Of course, the test fails because we event doesn't have a component created yet. 

test case of the above example

Add Component: src/components/Missions/Missions.js

1import React from "react"
2 
3export const Missions = () => {
4   return <div>
5       Missions component should be here.
6   </div>
7}
8 
9export default Missions;
10

Now the test is passing

test case of the above example

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. 

1// src/components/Missions/__tests__/Missions.spec.js
2
3Import MockedProvider:
4 
5import { MockedProvider } from '@apollo/client/testing';
6

Define mocks: 

1const mocks = [
2   {
3       request: {
4           query: GET_MISSIONS,
5           variables: {
6               limit: 3,
7           },
8       },
9       result: {
10           "data": {
11               "missions": [
12               {
13                   "id": "9D1B7E0",
14                   "name": "Thaicom",
15                   "twitter": "https://twitter.com/thaicomplc",
16                   "website": "http://www.thaicom.net/en/satellites/overview",
17                   "wikipedia": "https://en.wikipedia.org/wiki/Thaicom"
18               },
19               {
20                   "id": "F4F83DE",
21                   "name": "Telstar",
22                   "twitter": null,
23                   "website": "https://www.telesat.com/",
24                   "wikipedia": "https://en.wikipedia.org/wiki/Telesat"
25               },
26               {
27                   "id": "F3364BF",
28                   "name": "Iridium NEXT",
29                   "twitter": "https://twitter.com/IridiumBoss?lang=en",
30                   "website": "https://www.iridiumnext.com/",
31                   "wikipedia": "https://en.wikipedia.org/wiki/Iridium_satellite_constellation"
32               }
33               ]
34           }
35       }
36   },
37];
38

The test fails because we don't have the GET_MISSIONS query defined yet.

client side query

Create the file queries/missions.gql.js with the following content:

1import { gql } from '@apollo/client';
2 
3export const GET_MISSIONS = gql`
4   query getMissions ($limit: Int!){
5       missions(limit: $limit) {
6           id
7           name
8           twitter
9           website
10           wikipedia
11       }
12   } 
13`;
14 
15

Import query in the src/components/Missions/__tests__/Missions.spec.js

1import { GET_MISSIONS } from "../../../queries/missions.gql";
2

Now let's wrap the Missions component by the Mocked provider. 

1const { getByText } = render(
2   <MockedProvider mocks={mocks}>
3      <Missions/>
4   </MockedProvider>
5);
6

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 () => {
2       const { findByText } = render(
3           <MockedProvider mocks={mocks}>
4               <Missions/>
5           </MockedProvider>
6       );
7 
8       await findByText('Thaicom');
9       await findByText('Telstar');
10       await 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. 

data returned does not have specified text

Let's update the React component. 

First import useQuery hook, and GET_MISSIONS query in src/components/Missions/Missions.js

1import { useQuery } from "@apollo/client";
2import { GET_MISSIONS } from "../../queries/missions.gql";
3

Let's query for the data in the component body:

1const  { data } = useQuery(GET_MISSIONS, {
2       variables: {
3           limit: 3
4       }
5});
6

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.

1const shouldDisplayMissions = useMemo(() => {
2    if (data?.missions?.length) {
3        return data.missions.map(mission => {
4            return <div key={mission.id}>
5                <h2>{mission.name}</h2>
6            </div>
7        })
8    }
9
10
11    return <h2>There are no missions</h2>
12}, [data]);
13 
14

In the end, the Component needs to return shouldDisplayMissions:

1return shouldDisplayMissions;
2The full component code: 
3import React, { useMemo } from "react"
4import { useQuery } from "@apollo/client";
5import { GET_MISSIONS } from "../../queries/missions.gql";
6 
7export const Missions = () => {
8   const  { data } = useQuery(GET_MISSIONS, {
9       variables: {
10           limit: 3
11       }
12   });
13 
14   const shouldDisplayMissions = useMemo(() => {
15       if (data?.missions?.length) {
16           return data.missions.map(mission => {
17               return <div key={mission.id}>
18                   <h2>{mission.name}</h2>
19               </div>
20           })
21       }
22 
23       return <h2>There are no missions</h2>
24   }, [data]);
25 
26   return shouldDisplayMissions;
27}
28 
29export default Missions;
30

Now, the test pass!

data returned does not have specified text

The last thing for this step is to inject components into the app and see missions in the browser. 

1// App.js
2
3import Missions from './components/Missions';
4 
5function App() {
6 return <Missions/>;
7}
8 
9export default App;
10

It works but, initially, it shows, "There are no missions." Let fix it by adding a loading indicator in Missions.js. 

First, grab the loading flag from the useQuery hook results: 

1const  { data, loading } = useQuery(GET_MISSIONS, {
2    variables: {
3        limit: 3
4    }
5});
6

Add loading indicator: 

1if (loading) {
2    return <p>Loading...</p>
3}
4
5return shouldDisplayMissions;
6

Besides, add a little bit of styling. 

1//src/components/Missions/Missions.module.css
2.mission {
3   border-bottom: 1px solid black;
4   padding: 15px;
5}
6

Now, import CSS module in Missions.js file: 

1import classes from './Missions.module.css'
2

and add mission class to the mission div:

1return <div key={mission.id} className={classes.mission}>
2    <h2>{mission.name}</h2>
3</div>
4
5

Here is the result: 

data returned does not have specified text

Add local-only field

OK, so we have data from API. The next task is to display links for the Mission. API returns three fields: 

  • - twitter

  • - website

  • - Wikipedia

We can create our local field called: links. It will be an array with links, so we can loop through that array and just display links. 

First, let's add a new test case:

1it ('Should display links for the mission', async () => {
2    const localMocks = [
3        {
4            ...mocks[0],
5            result: {
6                data: {
7                    missions: [
8                        {
9                            "id": "F4F83DE",
10                            "name": "Telstar",
11                            "links": ['https://www.telesat.com/']
12                        },
13                    ]
14                }
15            }
16        }
17    ]
18    const { findByText } = render(
19        <MockedProvider mocks={localMocks}>
20            <Missions/>
21        </MockedProvider>
22    );
23
24
25    await findByText('https://www.telesat.com/"')
26});
27

So, we expect that there will be rendered one link: "https://www.telesat.com/"

Define field policy

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:

data returned does not have specified text

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({
2 uri: 'https://api.spacex.land/graphql/',
3 cache: new InMemoryCache({
4   typePolicies: {
5     Mission: {
6       fields: {
7         links: {
8           read(_, { readField }) {
9             // logic will be added here in the next step
10           }
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. 

Our logic for the links local field:

1read(_, { readField }) {
2    const twitter = readField('twiiter');
3    const wikipedia = readField('wikipedia');
4    const website = readField('website');
5    const links = [];
6
7
8    if (twitter) {
9      links.push(twitter);
10    }
11
12
13    if (wikipedia) {
14      links.push(wikipedia);
15    }
16
17
18    if (website) {
19      links.push(website);
20    }
21
22
23    return links;
24  }
25

The query for local-only field

The next step is to include the links field in the query. Let's modify the GET_MISSIONS query:

1query getMissions ($limit: Int!){
2    missions(limit: $limit) {
3        id
4        name
5        twitter
6        website
7        wikipedia
8        links @client
9    }
10} 
11

You can define the local-only field by adding the @client directive after the field name. 

Display local-only field on the screen

We have made good progress, but the test still fails because the Component does not render any links yet.

apollo - managing local state - test case

Please update the Missions component by modifying the shouldDisplayMissions Memo function.

1const shouldDisplayMissions = useMemo(() => {
2    if (data?.missions?.length) {
3        return data.missions.map(mission => {
4            const shouldDisplayLinks = mission.links?.length ? mission.links.map(link => {
5                return <li key={`${mission.id}-${link}`}>
6                    <a href={link}>{link}</a>
7                </li>
8            }) : null;
9
10
11            return <div key={mission.id} className={classes.mission}>
12                <h2>{mission.name}</h2>
13                {shouldDisplayLinks}
14            </div>
15        })
16    }
17
18
19    return <h2>There are no missions</h2>
20}, [data]);
21

We are good now. Everything work as well in the browser:

apollo - managing local state - test case

And tests pass:

managing local state - all tests pass

Working Demo

Here you can see the demo of the app:

https://apollo-client-local-only-fields-tutorial.vercel.app/

Source code

Here you can find the source code for this tutorial: https://github.com/Frodigo/apollo-client-local-only-fields-tutorial

Here are the commits for each step:

  1. 1. Initialize the project using Create React App

  2. 2. Install Apollo Client

  3. 3. Initialize Apollo Client

  4. 4. The query for missions data

  5. 5. Add local-only field

Reactive variables

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.

apollo local state manement - example case to use

Implementation

First, let's define the reactive variable:

1import { makeVar } from "@apollo/client";
2
3export const CART_ID_IDENTIFER = 'currentCardId'
4export const cartId = makeVar(localStorage.getItem(CART_ID_IDENTIFER));
5

Second, use that variable in a component, context, or hook and make it reactive:

1import { useReactiveVar } from '@apollo/client';
2
3export const CartProvider = ({ children }) => {
4    const currentCartId = useReactiveVar(cartId);
5};
6

Third, define the mutation to collect a cart Id from the server:

1// mutation: 
2import { gql } from '@apollo/client';
3
4export const CREATE_EMPTY_CART = gql`
5    mutation createEmptyCart {
6        createEmptyCart
7    }
8`;
9
10// component
11// imports
12import { useMutation } from '@apollo/client';
13import { CREATE_EMPTY_CART } from '../../mutations/cart.gql'
14
15// ... in component body
16const [ createEmptyCart ] = useMutation(CREATE_EMPTY_CART, {
17    update(_, { data: { createEmptyCart } }) {
18        cartId(createEmptyCart);
19        localStorage.setItem(CART_ID_IDENTIFER, createEmptyCart);
20        }
21});
22

I used the update callback there, and I updated the reactive variable:

1cartId(createEmptyCart)
2

Then I also updated a value in the local storage.

Last, check if the cart ID exists in the local state and if not, send the mutation to a sever.

1const currentCartId = useReactiveVar(cartId);
2
3useEffect(() => {
4    if (!currentCartId) {
5        createEmptyCart();
6    }
7}, [createEmptyCart, currentCartId]);
8

Summary

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.

Read more