2 Comments

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.