Simplifying Apollo Client Local Cache with React Hooks

Recently, I used Apollo Client local cache on a project. Like most features of Apollo Client, it’s powerful but complicated. For my project, I wanted to save a blob of data that represents the current state of a page. This blob would allow a user to navigate away from a page and come back later without losing state. The page retrieves the state from Apollo Client cache.

Apollo Client cache seemed like a good fit for this problem. I was already using Apollo to fetch data from my server, so it was convenient to fetch local data with the same mechanisms. Also, Apollo automatically refreshes the data for any component by looking at an updated piece of state.

However, it was difficult to get started with local state management in Apollo. I followed the documentation from Apollo, but I struggled to use the write function. The main struggle was making sure that components that relied on a piece of state would update automatically when the value of the state changed.

I eventually figured it out, but I wanted a simpler interface.

To make Apollo Client cache easier to work with, I created a custom React hook that works very similar to React’s own useState hook.


export function usePersistentState(bucket) {
  const client = useApolloClient();
  const query = queryMap[bucket];
  // useQuery is a hook supplied by Apollo Client.
  // Using this hook allows the component to watch for updates to the state
  const queryResult = useQuery(query).data;
  const state = queryResult && queryResult[bucket];

  const setState = (value) => {
    try {
      const data = {
        [bucket]: {
          __typename: bucket,
          ...value,
        },
      };
      client.writeQuery({ query, data });
    } catch (error) {
      // Add whatever custom error handling your application needs...
      console.error(error);
    }
  };

  return [state, setState];
}

const queryMap = {
  counterState: counterQuery,
  // more buckets can go here...
};

const counterQuery = gql`
query CounterStateQuery {
  counterState {
    count
  }
}
`;

And here’s the hook in action:


export const Counter = props => {
  // The custom hook that is backed by Apollo Client cache
  const [state, setState] = usePersistedState("counterState");

  const onIncrementClick = React.useCallback(() => {
    const newState = {
       ...state,
       count: state.count,
    };
    setState(newState);
  });

  return (
    <div>
       <span>Count: {state.count}</span>
       <button onClick={onIncrementClick}>Increment</button>
    </div>
  );
};

Above, I refer to the argument of usePersistedState as a “bucket” where I want to get data. I like the visual of putting data in a bucket and leaving it alone until I need it again. You can view the state and modify other components, too, if necessary.

What I really like about this approach is its compatibility with React’s useState. Usually, I’ll start building a component with useState, and if I need to have the component’s state persisted, I’ll switch it over to usePersistedState.

I also created a “reducer version” of usePersistedState, which is similar to React’s useReducer.


export function usePersistentStateReducer(
  bucket,
  reducer
) {
  const [state, setState] = usePersistentState(bucket);

  const dispatch = useCallback(
    (action) => {
      return setState(reducer(state, action));
    },
    [reducer, state]
  );

  return [state, dispatch];
}

Since React has introduced hooks, I’ve really enjoyed developing in it. Creating custom hooks is probably my favorite part of web development at the moment, and I think this one is a great example of how useful they can be.

Conversation
  • Denis Susloparov says:

    Isn’t it squirrel hunting with a howitzer? A simple snippet like https://gist.github.com/mjackson/f2ab605e3cb13ab069059d169cdc093a does absolutely the same, except the performance overhead and routine of writing a query for every bucket of course :). if you need multi-tab support with hot state updates this is a much more straightforward solution: https://github.com/donavon/use-persisted-state

    • Andy Peterson Andy Peterson says:

      Thanks for the response, Denis! I agree with your assessment depending on the context of the codebase.

      I think the strategy I described in this post is overkill if your project wasn’t using Apollo already. My team used the approach above because we already have the infrastructure built. We wanted to have a single data store for our web client, which gives us consistent data access patterns for all the data on the client. It also allows us to write a query that fetch data both from local client state and external data sources. We also are generating TypeScript types from our GraphQL schema, so this approach is giving us some type-safety with much effort.

      We don’t have the need for the cross tab support, but that is an interesting use case I did not consider. Thanks for bringing that up.

      If I didn’t have Apollo setup on my project, or if I wanted to make the tradeoff of maintaining multiple data stores, I would consider using the strategies you mentioned. Thanks for sharing!

  • Comments are closed.