State Management through Apollo Client

A Series Blog for Deep Dive into Apollo GraphQL from Backend to Frontend

Photo by Martin Behrendt on Unsplash

This is a 9th of the series blogs on deep dive into Apollo GraphQL from backend to frontend. A lot of information is Apollo GraphQL Doc or GraphQL Doc as well as their source code on Github — all tributes go to them. For my part, I would like to give you my “destructuring” of the original knowledge and my reflection on it, analysis on the code examples/source code as well as some extra examples.

In my last blog, we are talking about the cache mechanism in Apollo Client, and its usage for storage. In this blog, let’s look at state management in Apollo Client.

Assuming you are familiar with a frontend Framework like React and a state management tool like Redux or simply React’s hooks, you know that a state management tool is to:

  • handle storage for local/remote state;
  • update state based on actions (CQSP);
  • enable reactivity to align the change of state in the store with the UI

For Apollo GraphQL Client, you will have the cache API to handle the first bit, the operations (mutations, queries, subscriptions) to handle the 2nd bit, and for the 3rd bit, while all the operations from the 2nd bit are broadcast by default, you can be more proactive and use the reactive variables and customised field policies to enhance that.

For all the incoming operations, Apollo will first check if the data is cached already in the local storage before send requests to the server. To better support this flow, Apollo Client 3 introduces two complementary mechanisms for managing local state: field policies and reactive variables.

Field policies enable you to define what happens when you query a particular field, including fields that aren’t defined in your GraphQL server’s schema. By defining field policies for these local-only fields, you can populate them with data that’s stored anywhere, such as in localStorage or reactive variables.

Reactive variables are just like React useContext, once you define it, you can import and update it wherever you need. Since it’s not stored in the Apollo Client cache, so it doesn’t need to conform to the strict structure of a cached type, nor does query it need to go through GraphQL operations.

  • Creating with the makeVar method: const cartItemsVar = makeVar([]);
  • Reading call the function returned by makeVar : cartItemsVar())
  • Modifying: call the function returned by makeVar with one argument : cartItemsVar([456])
  • Reacting : const cartItems = useReactiveVar(cartItemsVar)

While it is totally ok to use field policies and react variables separately, in some cases they are more powerful when used together. Let’s see an example from the Doc.

Let’s see we have a list of items we want to store in the local cart.

1. Initialise a reactive variable to store our local piece of data that we want to subscribe the changes toexport const cartItemsVar = makeVar([]);2. Connect the Reactive Variable to a cache policy.export const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
cartItems: {
read() {
return cartItemsVar();
}
}
}
}
}
});
3. A button component that enables the user to add a product to their cart. On click, this button updates the value of cartItemsVar to append the button's associated productId.import { cartItemsVar } from '.export function AddToCartButton({ productId }) {
return (
<div>
<Button onClick={() =>
cartItemsVar([...cartItemsVar(), productId])}>
Add to Cart
</Button>
</div>
);
}
4. Subscribing to the value of a Reactive Variable
4.1. For reading the updated values of cartItems , we make our operations consistent by going through the normal Apollo GraphQL query like so:
export const GET_CART_ITEMS = gql`
query GetCartItems {
cartItems @client
}
`;
export function Cart() {
const { data } = useQuery(GET_CART_ITEMS);
return (
<div class="cart">
{data && data.cartItems.map(productId => (
<CartItem key={productId} />
))}
</div>
);
}
4.2. Alternatively, you can read directly from a reactive variable using the useReactiveVar hook introduced in Apollo Client 3.2.0:import { useReactiveVar } from '@apollo/client';export function Cart() {
const cartItems = useReactiveVar(cartItemsVar);
return (
<div class="cart">
{cartItems.map(productId => (
<CartItem key={productId} />
))}
</div>
);
}

You don’t always need to use the reactive variables to store local only fields. As long as the record is in the Cache store, and the Cache store knows where to find the record, it is ok with it. Note that in the example below we didn’t have a read function for isLoggedIn , that’s because by default the cache will return the field value with the same name.

1. Write the data into cache with cache.writeQuery.const cache = new InMemoryCache();
const client = new ApolloClient({
uri: 'http://localhost:4000/graphql',
cache
});
const IS_LOGGED_IN = gql`
query IsUserLoggedIn {
isLoggedIn @client
}
`;
cache.writeQuery({
query: IS_LOGGED_IN,
data: {
isLoggedIn: !!localStorage.getItem("token"),
},
});
2. Query the field ReactDOM.render(
<ApolloProvider client={client}>
<App />
</ApolloProvider>,
document.getElementById("root"),
);
function App() {
const { data } = useQuery(IS_LOGGED_IN);
return data.isLoggedIn ? <Pages /> : <Login />;
}

Since we can combine local + remote data together in the Apollo Client cache store before returning to the UI, it’s good to know how to organise the two different kinds of data.

In general, you can use Apollo Client as:

  • Pure local state management tool without connecting to a remote GraphQL schema;
  • Both local and remote data store management where local data is separate from remote schema;
  • Both local and remote data store management where local data is used to decorate remote data.

The differences between 2 and 3 is only depends on how you define your schema. For example for a todo app that has the todos fetched from a remote server, option 2 can have a irrelevant preferredTea field that is logically advisable separate from the todos cache field. For option 3, you may have a completedTodos local field that will filter the data based on if the todo is completed or not with a tick box from the user interaction on the frontend UI.

That’s all for it today.

Happy Reading!

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store