Improving performance with React Apollo client

In this article, we are going to look at cache normalization with apollo client in order to improve client-side performance and reduce redundant API calls.

What is Apollo and Why do we need it

So talking about what is Apollo client, it's a library that simplifies graphQl calls in the frontend, but yes you can directly call the server and send graphQl query as the body it will still be executed but to make it more scalable and readable it is generally preferred to use a library which can handle graphQl calls.

Setting up Apollo client with React

Let's start with installing the dependencies

npm install @apollo/client graphql

OR

yarn add @apollo/client graphql

After installing these we have to set up the graphQl with authentication methods and general rules which will be required to enable cache.

Create a file inside src/ named initialize.client.js I generally prefer creating a separate folder name helper and then adding the file in the helper folder but its totally optional.

Now lets add this code in intialize.client.js

import { ApolloClient, createHttpLink, InMemoryCache } from '@apollo/client';
import { setContext } from '@apollo/client/link/context';


const httpLink = createHttpLink({
    uri: process.env.REACT_APP_GRAPHQL_ENDPOINT,
});

const authLink = setContext((_, { headers }) => {
    const token = localStorage.getItem('auth-token');
    return {
        headers: {
            ...headers,
            authorization: token ? `Bearer ${token}` : "",
        }
    }
});

export const gqlClient = new ApolloClient({
    link: authLink.concat(httpLink),
    cache: new InMemoryCache()
})

Now let's understand the code piece by piece

const authLink = setContext((_, { headers }) => {
    const token = localStorage.getItem('auth-token');
    return {
        headers: {
            ...headers,
            authorization: token ? `Bearer ${token}` : "",
        }
    }
});

here in this code, we are creating an object which will contain all the additional headers that are required the best part here is apollo will automatically verify if the person is authenticated in the first place since we are checking the local storage for the token there are multiple ways to store a token but in this article, we will be focusing on the cache normalization.

The second part of the code

export const gqlClient = new ApolloClient({
    link: authLink.concat(httpLink),
    cache: new InMemoryCache()
})

Similarly, in this piece, we are creating a new instance of apollo client where we are passing the newly created link and the important part cache here we are asking apollo to cache all the queries that are being made to optimize the performance and reduce the network calls.

Finally, all the setup is done now let's add the client in the context to use it in the whole application

in index.js add ApolloProvider and pass the client we created as prop

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import { ApolloProvider } from '@apollo/client';
import { gqlClient } from './utils/initialize.client';

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
root.render(
  <React.StrictMode>
    <ApolloProvider client={gqlClient}>
      <App />
    </ApolloProvider>
  </React.StrictMode>
);

We are good to go.

But before going ahead let's understand how caching works.

Apollo actually stores all the queries/mutations with its returning __typename

let's have a look at a mutation

mutation updateCartMutation {
  update_cart(where: {id: {_eq: 1}}, _set: {quantity: 1}) {
    returning {
      id
      product_id
      quantity
    }
  }
}

here update_cart is the __typename that is going to be unique with its parameters like id and quantity now apollo uses all these parameters and stores it in its local state along with the returning row.

generally, apollo updates the state automatically if it is a flat state like a boolean or a number but when it comes to a nested object that's where all the complex logic is required because the cache updation has to be done manually.

Trying it real-time let's make a query to our DB using the hooks that are provided by apollo-client we will use useQuery hook to fetch products from the backend

image.png

Response of the query

image.png

here focus on the __typename it is products we will now try to update the product name in the backend as well as update the cache

In order to execute a mutation, we will use the useMutation hook provided by apollo-client. This hook takes a mutation as a parameter and returns a function where we pass the variables and all the desired params

const [UpdateProductMutation] = useMutation(UpdateProduct)

now we will call this function whenever we would like to update the product, but before that let's construct the mutation function it takes an object as a parameter which will contain variables and other different methods.

Note: the variable name should be the same as the name in the graphQl mutation example:

 mutation UpdateProduct($name: String = "", $id: Int = 10) {
  update_products(where: {id: {_eq: $id}, name: {_eq: $name}}) {
    returning {
      name
    }
  }
}

The function looks like this

  function updateProduct(){
    UpdateProductMutation({
      variables:{
        id:1,
        name:"Nike Shoes"
      }
    })
  }

this can be enough but apollo provides advanced response handling methods like onError, onComplete. Similarly, it provides a method named update which is only executed when a mutation is executed successfully.

This update method will be used to update the cache without making a extra call.

That's how the whole mutation looks

UpdateProductMutation({
      variables:{
        id:1,
        name:"Nike Shoes"
      },
      update:(cache,{data})=>{
        console.log({cache})
        console.log({data})
      }
    })

the update methods take 2 params one is cache existing data in memory and the other params returns an object with a bunch of different properties but we will take out the updated data which is directly related to returning rows in mutation

let's check how the cache and data look like

image.png

The cache contains all the data and a bunch of different methods whereas data contains the affected row data

Now let's update the cache in 2 steps

  1. Read existing query
  2. Update query
  const { data } = useQuery(GetProducts)

  useEffect(() => {
    console.log({ updatedData: data })
  }, [data])

  const [UpdateProductMutation] = useMutation(UpdateProduct)

  function updateProduct() {
    UpdateProductMutation({
      variables: {
        id: 1,
        name: "Nike Shoes"
      },
      update: (cache, { data }) => {
        const cachedQuery: any = cache.readQuery({ query: GetProducts })
        console.log({ beforeUpdate: cachedQuery })
        if (cachedQuery) {
          cache.writeQuery({
            query: GetProducts, data: {
              products: cachedQuery.products.map((product: any) => product.id === 1 ? ({ ...product, name: "Nike Shoes" }) : product)
            }
          })
        }
      }
    })
  }

Remember the key inside data should always be same as the __typename of the query

Before Update image.png

The update caused a re-render resulting in a useEffect run

After Update image.png

Yes we have successfully updated there are other ways as well you can use refetch method returned by useQuery hook but it adds a API which you would want to avoid.