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
Response of the query
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
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
- Read existing query
- 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
The update caused a re-render resulting in a useEffect
run
After Update
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.