Apollo local state management in React using GraphQL

srinivasarao aleti
EverestEngineering
Published in
8 min readJan 11, 2020

Local state management in React has always been a vital yet somewhat dreadful aspect of working with React. There some popular libraries like Redux, Mobx .. etc will help us to manage the local state to React. After React hooks came into the market React.ContextApi becomes more popular.

Frankly speaking, I am not a big fan of Redux because it enforces me to write more code. I have to change 3 to 4 files to write a single helloworld to redux store. It is a hell to manage async actions. I become a big fan of React.ContextApi once I started using it. I highly suggested using React.ContextApi with hooks.

Recently I did a spike on Apollo local state management in one of my projects.
I was amazed by the way it works 😃. Thanks for apollo dev tools too 👏. Let’s look into it.

In simple words

  • Apollo Client (>= 2.5) has built-in local state handling capabilities that allow you to store your local data inside the Apollo cache alongside your remote data.
  • Maintains a single global store which can be accessed by all the components just like Redux.

Required Packages:-

graphql:-
GraphQL
is a query language (that’s what the “QL” stands for) for APIs and a runtime for fulfilling those queries with your existing data. If you don’t know anything about GraphQL, please go ahead and learn it first.

Visit to learn more about Graphql: https://graphql.org/

Install it using:
npm install graphql

apollo-boost:-
Apollo Boost is a zero-config way to start using Apollo Client. It includes some sensible defaults, such as our recommended InMemoryCache and HttpLink, which come configured for you with our recommended settings.

Install it using:
npm install apollo-boost

@apollo/react-hooks:
I am a big fan of React hooks. Believe me, hooks reduces a lot of the duplication in components. @apollo/react-hooks providesuseQuery, useLazyQuery, useMutation, useSubscription and useApolloClient hooks.

Install it using:
npm install @apollo/react-hooks

Configuration:-

The configuration is pretty simple.
* Set up ApolloClient.
* Pass the client to ApolloProvider.
* Wrap the code as a child of ApolloProvider.

Usually, we set up the apollo client at the root component so that the store will be available to all the components. Here is a simple code to set up the apollo client.

import React from ‘react’;
import { ApolloProvider } from ‘@apollo/react-hooks’;
import ApolloClient from ‘apollo-boost’;
const client = new ApolloClient(); //setting up clientfunction App() {
return (
<div className=”App”>
<ApolloProvider client={client}> //pass the client to provider
<h2>My first Apollo app 🚀</h2> //wrapping the components
</ApolloProvider>
</div>
);
}

Initializing the cache:

Some times we need to initialize the cache with an initial state. For example, if we want to implement a counter, then we have to keep `count:0` initially. In that case, the client looks like this.

import { InMemoryCache } from 'apollo-cache-inmemory';const cache = new InMemoryCache()const client = new ApolloClient({  
cache,
});
const initialState = {
counter: 0;
}
cache.writeData({ data: initialState });

Querying:

Querying for local data is very similar to querying your GraphQL server. The only difference is that you add a @client directive on your local fields to indicate they should be resolved from the Apollo Client cache or a local resolver function.

Previously we kept `count:0` in the cache. One can query it using below code

Ex: 
const GET_COUNTER = gql`
query GetCounterValue {
counter @client
}`;

Note: Since the counter is resolved from local cache we need to use @client directive. One cool feature with Apollo is, it allows us to combine server, client data resolvers in the same query.

Updating local state:-

There are two main ways to perform local state mutations.

Direct Writes:

  • Directly write to the cache by calling cache.writeData .
  • Ideally, we use this approach for mutations that don’t depend on the data that’s currently in the cache

Ex: Previously when we want to keep {counter:0} in the cache initially we used cache.writeData

useMutation Hook:

  • useMutation hook with a GraphQL mutation that calls a local client-side resolver.
  • At first, we need to write a Graphql mutation and it will be passed to useMutation.
const UPDATE_COUNTER = gql`
mutation updateCounter($offset: Number!) {
updateCounter(offset: $offset) @client
}
`;
  • Ideally, we use this approach for mutations that depend on the data that’s currently in the cache

Example: Let’s say we want to increment the counter by 1. In that case, we need to query the existing counter in the cache and increment it by 1 then write the new value back to the cache. Check the below example for more info.

const UPDATE_COUNTER = gql`
mutation updateCounter($offset: Number!) {
updateCounter(offset: $offset) @client
}
`;
//Increment Mutation
const [increment] = useMutation(
UPDATE_COUNTER, { variables: { offset: 1 } }
)
//Decrement Mutation
const [decrement] = useMutation(
UPDATE_COUNTER, { variables: { offset: -1 } }
)

Note: variables in the mutations helps us to pass arguments to the mutations. In our example, the variables object holds offset.

Local resolvers:-

Local resolvers hold the mutations which will mutate a value in the cache. Here is how a client with a local resolver looks like.

const GET_COUNTER = gql` 
query GetCounterValue {
counter @client
}
`;
const cache = new InMemoryCache();//All the mutations goes here
const mutations = {
updateCounter: (_, variables, { cache }) => {
//query existing data
const data = cache.readQuery({ query: GET_COUNTER });
//Calculate new counter value
const newCounterValue = data.counter + variables.offset;
cache.writeData({
data: { counter: newCounterValue }
});
return null; //best practices
}
}
const client = new ApolloClient(
{
cache: new InMemoryCache(),
resolvers: {
Mutation: mutations
},
}
);
cache.writeData({ data: initialState });

Note: variables in the mutations helps us to pass arguments to the mutations. In our example, the variables object holds offset.

The above example illustrates how one can set up an apollo client with cache, initial state, local resolvers and mutations.

It is completely okay to put all together in a single file for the small applications Counter application. Imagine what if you want to implement a Todos application alongside Counter?. What will happen if we keep local resolvers requires for todos application in the same file? What if dealing with big applications?. Keeping all the code in a single file is not a stable solution. It is always better to maintain a logically separated project structure. Lets how we can tackle down above problem? How we can restructure out code?

Project Structure:

The project structure is a crucial part when it comes to an application. Here I am attaching how I structured my src folder for a simple counter application with Apollo Local State Management. You might not need this kind of structure for a simple application, but when you think in terms of the big project then this kind of structure will definitely help you.

I want to mainly focus on Apollo folder. I divided the code I wrote inLocal Resolver section into three categories

  • Apollo/Queries:-

All the queries in our application go here. All queries related to a major component goes into a file. Since we have the only query related to Counter I keep it inside Apollo/Queries/CounterQueries/index.js.
Let's say if we have some other queries related to Todos then it goes to
Apollo/Queries/TodosQueires/index.js.

Apollo/Queries/CounterQueries/index.js

import gql from "graphql-tag";export const GET_COUNTER = gql`
query GetCounterValue {
counter @client
}
`;
  • Apollo/Mutations:
    Mutations also follow a similar pattern that queries does follow. All the mutations in our application goes to Apollo/Mutations.
    Counter Mutations: Apollo/Mutations/CounterMutations/index.js.
    Let’s say if we have some other Mutations related to Todos then it goes under
    Apollo/Mutations/TodosMutations/index.js.

Apollo/Mutations/CounterMutations/index.js

import gql from "graphql-tag";
import { GET_COUNTER } from "../../Queries/CouterQueries";
export const UPDATE_COUNTER = gql`mutation updateCounter($offset: Number!) {
updateCounter(offset: $offset) @client
}`;
export const CounterMutations = {
updateCounter: (_, variables, { cache }) => {
//query existing data
const data = cache.readQuery({ query: GET_COUNTER });
//Calculate new counter value
const newCounterValue = data.counter + variables.offset;
cache.writeData({
data: { counter: newCounterValue }
});
return null; //best practice
}
};
  • Configuration (client & local resolvers):
    So far we separated queries and mutations, now it’s time to set up the final configuration for apollo client. The configuration resides in Apollo/config.js
import ApolloClient from "apollo-boost";
import { InMemoryCache } from "apollo-cache-inmemory";
import { CounterMutations } from "./Mutations/CounterMutations";
const cache = new InMemoryCache();const client = new ApolloClient({
cache,
resolvers: {
Mutation: {
...CounterMutations
}
}
});
const initialState = { counter: 0 };
cache.writeData({ data: initialState });
export default client;

Note: If you want to add a new mutations to the configuration then only Mutations object will changes. Let’s say you wanted to add Todos Mutations then we will add it in Mutations object.

Mutation: {
...CounterMutations,
...TodosMutations
}

Now you understood how we can use Apollo Local State Management in React?. Now it is time to verify the counter application in actions. You can visit the below link for complete code.

Please visit this link : https://codesandbox.io/s/react-example-zm62u

Pros:

  • Zero configuration:- You can quickly setup apollo local state management, You just need to install a couple of npm packages. No need to write extra configuration.
  • Declarative data fetching: With Apollo’s declarative approach to data fetching, all of the logic for retrieving your data, tracking loading and error states, and updating your UI is encapsulated by the useQuery Hook
  • Combine local & server data:- one of the big advantages of using apollo local state management is one can use the same Query/Mutation to fetch data from both servers as well as the local state.
query {
books {
id
author
title
selected @client
}
}

When the above query executed on server-side, only {id, author, title} will be queried. selected will not be queried on the server. When it is executed on local state then {id, author, title, selected} will be queried.
@client directive will tell apollo to execute the given field only on local state.

Cons:

  • The biggest problem with Apollo-Link-State is that it is quite verbose especially in the beginning.

Example:
If you want to use write fragment you need to include the __typename in the data, if you use write query the items inside the query need to contain an id.

  • I felt like apollo local state management is a little bit difficult to understanding in beginning. There is very few articles are available on the internet.

For more examples on this topic visit this link: https://github.com/srinivasaleti/apollo-local-state-management

Summary:

  • Apollo Client (>= 2.5) has built-in local state handling capabilities that allow you to store your local data inside the Apollo cache alongside your remote data.
  • Apollo client configuration is pretty simple.
  • React Hooks with Apollo provide an even easier way to query the data from the server and update it.
  • Queries help us to query data from the cache.
  • Mutations help us to modify the cache.
  • Apollo provides excellent dev-tools which helps us a lot while debugging cache.

--

--