Updating Specific Queries in ApolloClient Cache and Re-rendering Data | by Mark Sauer-Utley | Towards Data Science

One of the nicest things about using ApolloClient is the automatic in-memory caching you get out of the box. When paginating your data, this allows you to go back to previous pages and have the transition be incredibly snappy. But sometimes you don’t want this data to stay cached forever. Sometimes you want to be able to empty the cache and fetch fresh data from your server. You also want this to trigger a re-render of whatever component is rendering out this data.
This was a situation I found myself in recently while building a data-visualization tool for my company. We have a web-crawler that crawls publisher’s webpages 24/7 and updates several different databases and tables. We can manipulate how this crawler behaves using various regex, xpath, and metadata configs that we give it for each individual publisher.
The issue we faced is that, when making changes to these configs, you want to be able to see the effect your changes have in all of these various databases and tables being updated by the crawler. Previously, we just manually wrote tons of SQL queries with tons of JOIN statements to see only the data we needed at any given point. This was pretty cumbersome and really slowed us down at times.
So, I built a server that would aggregate all of these databases and various tables for us into a graph (using Node.js, GraphQL, ApolloServer, and MySQL). Then I built a web-app that gave us tons of filters we could use to see only the data we wanted (using Next.js and ApolloClient).
Because we crawl websites from publishers, the app is called Pub Crawl.
It handles a ton of data. Here’s a sample query:
The main challenge with the frontend (due to ApolloClient’s awesome in-memory caching) was giving the user the ability to update that data at-will so they could check something, make a change in the configs, then see what effect it had.
Googling around for ways to “clear the Apollo cache” was not a good way to figure this out. Unfortunately, this is what I did.
I learned doing this that Apollo gives you access to a method on the cache object called resetStore(). This method will clear out your entire cache and refetch all of the queries from the server. I really wanted this to be the solution to my issue, because it seemed so easy.
It wasn’t though, which made me sad.
This is not the use-case resetStore() was built for. The problem here is that it does not trigger a re-render, so even though the cache is full of fresh data, the user can’t see it. I wanted the user to click a button, see a loading screen, then see the new data. I also didn’t want to reset the entire cache because some of the data in the application wasn’t going to change.
Let’s pretend we have a page that shows us a dog. There is one dog in our database, but this dog changes. Maybe he gets a bath, maybe he rolls in dirt. But he is very active and his properties change frequently.
With the initial query, Apollo makes things so simple. You get access to some state with properties like loading, error, and data. It’s super nice. It lets you write code like this:
That’s very nice. Lovely developer experience.
But now we want to refresh our dog with a button. Let’s add a button to our jsx:
So what does that button do?
Well, it turns out that our useQuery() hook also gives us access to a function called refetch(). This function is awesome. It lets you refresh the result of just this one query, or even pass in new arguments to it and get that result. This solves my issue from before of clearing the entire cache, but it does not flip that loading boolean back on for us. So it will only re-render when the data is finished fetching. That was a problem for me because I needed my users to know that their data was reloading and not just trust the button. It’s possible that the data doesn’t change, in which case the user would not see anything happen!
After a lot of hacking, I found out that Apollo gives us a tool to trigger our re-render when the refetch starts. This is the networkStatus that is returned by the useQuery() hook. To use it, we need to pass an option to the hook that tells it to alert us when the networkStatus changes (which it will while refetching). This option is notifyOnNetworkStatusChange.
Now we can say “if the network status is re-fetching, show the loading component”. The status code for this is 4. So we can add that into our loading check:
Now let’s make a click-handler for that button that calls the refetch() function:
import { useQuery } from '@apollo/react-hooks';import Loading from './Loading';import Error from './Error';import Dog from './Dog';import { DOG_QUERY } from '../lib/queries';export default () => {  const { data, error, loading, refetch, networkStatus } = useQuery(    DOG_QUERY,    { notifyOnNetworkStatusChange: true }  );        if (loading || networkStatus === 4) return <Loading />;  if (error) return <Error error={error} />;    return (    <>      <button         type="button"         role="button"        onClick={() => refetch()}      >        See what's new with the dog      </button>      <Dog dog={data.dog} />    </>  );}
And that’s it. Now, when we click the button, we will see the <Loading /> component, the cache will refresh, and then we will see our new dog data in our <Dog /> component.
I hope this helped you. I know I got stuck on this for a couple hours solely because I was googling the wrong words. Hopefully, you don’t have the same issue!
Feel free to follow me on Twitter @sour_utley.
If you (for some strange reason) want to look at the repo for the tool I made for my company, it lives here. Also, in case you had any issues with the iframes from Carbon, here’s the code as right here on the page:
import { useQuery } from '@apollo/react-hooks';import Loading from './Loading';import Error from './Error';import Dog from './Dog';import { DOG_QUERY } from '../lib/queries';export default () => {  const { data, error, loading, refetch, networkStatus } = useQuery(    DOG_QUERY,    { notifyOnNetworkStatusChange: true }  );        if (loading || networkStatus === 4) return <Loading />;  if (error) return <Error error={error} />;    return (    <>      <button         type="button"         role="button"        onClick={() => refetch()}      >        See what's new with the dog      </button>      <Dog dog={data.dog} />    </>  );}