Local state management with Apollo and React hooks.

Sean Rennie
8 min readOct 29, 2019

--

Using Apollo’s new react hooks to create simple to use local state in a react application. This one uses Typescript.

Edit: 2020–10–26: This article was using Apollo Client v2.x, since then Apollo have released v3.0 which come with much better local state management features (Reactive variables). I suggest you check it out.

I’ve recently started having a play with and using Apollo Client’s local state management features in my React app. I found that creating custom hooks as an abstraction over the cache read write logic made it really easy to use.

In this blog post I’m going to walk through writing two custom hooks. One for fetching data from the cache and one for writing new data to the cache. The example we will use is a store for the current location of the app. In my example the user can navigate between module/page locations from two places; the left hand draw or some deeply nested page within the app. I’ve put together a simple example that represents this type of navigation:

Simple example representing primary navigation on from the left draw and alternative navigation from some deeply nested child.

Right, before I get going it’s worth mentioning that this is not a tutorial. Its simply a walk through of how I’ve done things. Please feel free to give any constructive criticism in the comments or a few claps if you liked it.

For those of you who just want to look at the code you can find the repo here and a working code sandbox here

The bit that tells you what I need and what I’m assuming…

It will be helpful if the reader has some understanding of GraphQL and React hooks. I’m a particular fan of the Apollo library and would recommend starting there if you are unfamiliar with GraphQL. For hooks you can find a myriad of articles after a short google.

For simplicity I’ve scaffolded my app with create-react-app. I’m also going to use Typescript to make my DX really good with VS-code and material-ui to simplify building the UI.

As I’m using Apollo local state management I’ll also need @apollo/react-hooks for the client side query and mutation hooks, apollo-boost which makes for a quick set up of the Apollo cache (it comes with a bunch of good defaults) and I will also need graphql &graphql-tag.

create-react-app apollo-hooks-local-state-react --typescript

yarn add @apollo/react-hooks @material-ui/core apollo-boost graphql graphql-tag react

Update: Apollo are about to release v3.0 of the Apollo client where the hooks will be available at @apollo/client. I’m not 100% sure yet but I think apollo-boost, and graphql-tag have also been included in the new core package.

Heres where I show you how I set it up…

After running CRA and installing my dependancies I’m left with the usual CRA start point. I’ve stripped out the CSS as I’ll be using @material-ui which makes use of JSS.

Set up Apollo Client… the cache

The first thing I need to do is set up our Apollo cache and wrap the app in an ApolloProvider passing it the client that we create using apollo-boost. For simplicity I’m going to do all this in the root index.tsx file but you could hide the cache set-up away if you wanted to.

On line 7 we define out ApolloClient. Because we are using the local state we can define some defaults with the clientState property. This example only needs navLocation which I set to "home" as a default. Naturally this might become much more complex as the app grows.

An FYI, if we where to use local resolvers or type definitions we would import them and include them in clientState as typeDefs and resolvers respectively. It’s not all that clear in the docs, but both of them accept an array of types and resolvers in the same way Apollo server does.

Passing the client to the ApolloProvider puts it on the the context making the client available to our whole app. Later I will use the useApolloClient hook to access the client and write some localState.

Set up the shell of the app and its pages…

For the UI of my app I have an AppShell component which renders a navigation panel on the left of the screen. I takes any child components and renders them in the content area. I’ve created a SomeView component that gets wrapped by the AppShell all though in reality this is likely to be a parent router of sorts.

SomeView renders two buttons, NAVIGATE HOME and NAVIGATE TO MAP. Think of it as some deep child which navigates to a different module/page in the app. The navigation panel in AppShell shows us where we are in the app by indicating which module is selected and rendering its name to the top of the panel. Have a look at the sandbox example at the top for clarity.

Lets make some hooks…

Right, to keep track of which module is active I am going to use the cache to store either home or map. I’ve already initialised the cache with navLocation: “home” and put the cache on the context with ApolloProvider.

The navigation panel needs to know what the location is so it can highlight the current location and displays its name. To do this I need to retrieve the navLocation value out of the cache.

Writing useLocationState…

useLocationState will query the cache and return the value of navLocation. All I need is the useQuery hook from @apollo/react-hooks and gql from graphql-tag.

In a file call useNavLocation.tsx I have the following:

I define a GET_NAV_LOCATION query using the gql tag which parse’s the query string into a valid query document to be used by Apollo. The important bit is navLocation @client. navLocation is the key stored as our default clientState and whose value we are accessing with my query. The @client directive instructs the query to look locally for navLocation rather than sending a request to the server.

With the query defined I create the custom hook on line 11. By convention the hook is prefixed with use. This will also keep the linter happy when I call useQuery on line 12 because hooks can only be used inside function components or other hooks. useQuery takes the query as an argument and returns a data object (there are other returned values like loading and error. Neither of which I am concerned with here). The result I’m after will be at data.navLocation so I return the data object which I can de-structure to get navLocation when I invoke the hook in my app.

Gotcha: When I first tried implementing this query I thought it would be necessary to use useQuery's onCompleted method to put the result of the query on the hooks state. I’d then try and return the state once it had updated. This backfired, not only because it was overly verbose but also because, onCompleted will only fire if the hook/component is still mounted. Often the state change itself resulted in a UI change, un-mounting the component calling the hook. And here the gotacha: the onCompleted method does not run if the component is not mounted. Best is just to KISS.

Using useLocationState…

I’ve exported the hook so I can simply import it into AppShell and invoke the hook like this:

And now we have the navLocation which we can use to conditionally highlight the nav bar and change the title.

No for updating navLocation when we navigate.

Writing useLocationDispatch…

To update the state I need to have access to the Apollo client. I can get it with the useApolloClient hook provided by @apollo/react-hooks. With the client I can directly write to the cache using the client.writeData method. Have a look at the code below:

Admittedly I’ve over engineered this example by going the whole hog with a switch statement and dispatch actions. Forgive me for that but in reality things will be more complex and having that kind of control is useful.

So what is actually going on? The useNavLocationDispatch hook returns a dispatch function which takes an action as an argument. The action is an object of type NavLocationAction which has two properties, type and nextLocation. Finally I export the hook to be used anywhere I want to update the location state within my app.

NOTE: In this example we only have one possible type which is nextLocation. We could have as many as we like. Using a Typescript union is a great way to ensure only known actions are used with their respective payload types.

It’s also worth noting that this is a simple use case where I am only updating a single property in the cache. For that I don’t need to do any other data manipulation of the state. If we need to perform more complex tasks at mutation time, Apollo allows us to write client side resolvers in the same way that you can on the Apollo Server.

Making use of useNavLocationDispatch

Updating the state is now a simple task of importing the useNavLocationDispatch hook wherever it’s needed. Passing the action to the returned dispatch function and invoking that function during some event. The gist below shows how I’ve used it in the SomeView component which represents a deeply nested child navigating to a different module.

Invoking useNavLocationDispatch gives me access to the dispatch function. With handleClick I invoke it passing it an action of { type: "nextLocation", nextLocation: location }. Now each time I click either the map or the home button, the navigation panel will update. For added clarity I’ve also disabled the button for the current location by accessing the navLocation with the useNavLocationState hook.

A bit of magic from the Apollo cache means that the UI updates immediately. By default all queries are subscriptions to the cache. When I call client.writeData() in the useNavLocationDispatch hook it will broadcast to all queries subscribed the cache that there is new data, updating the UI.

Conclusion

I have found using Apollo Client’s local state solution to be relatively straight forward after a good read of the docs. Writing a few custom hooks as an abstraction over the cache logic makes it even easier to re-use wherever needed. I can only see this getting better as Apollo and the community improve on their local state management features. It has already come such a long way in the year that I have been using the library.

I have however found one use case where the Apollo cache is not a good fit. This is when you need to store a class which contains a number of methods you don’t have access to, like a map feature class from Google maps. I cover how I deal with this in more detail in an earlier blog, so have a read if you’re interested.

As always, keep an open mind and don’t get too dogmatic about one particular solution. I love the Apollo library and what the team have been doing for the GraphQL community, but letting the problem dictate the solution and not the other way around has served me well so far.

If you like what you’ve read, please leave some claps. Otherwise I would appreciate any feedback in the comments.

Shout out and big thanks to the Apollo team for the great work they are doing. Keep it coming!

--

--

Sean Rennie
Sean Rennie

Written by Sean Rennie

Software engineer with a love of maps and anything geospatial. I enjoy techno, the outdoors, tequila & coffee. Currently working at Sensat Ltd