Local state management with Apollo and React hooks.
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:
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
astypeDefs
andresolvers
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
'sonCompleted
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: theonCompleted
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 isnextLocation
. 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!