DEV Community

loading...
Cover image for Local State Management with Apollo Client

Local State Management with Apollo Client

Patrick Hund
Software engineer, cartoonist, electronic music producer. He/him.
Updated on ・7 min read

I'm building a social media network and collaboration tool based on mind maps, documenting my work in this series of blog posts. Follow me if you're interested in what I've learned along the way about building web apps with React, Tailwind CSS, Firebase, Apollo/GraphQL, three.js and TypeScript.

Today's Goal

In my previous posts, I've built a 3D mind map, using React and three.js.

For this project, I've used CodeSandbox exclusisvely for coding and I just love it! It works just like VS Code, but runs completely in the browser and lets me share my code easily in my blog posts.

Just one thing that bothered me as I worked on my mind map: Performance issues. The nature of rendering an interactive 3D model involves having a loop that continuously updates, running 60 times per second. CodeSandbox seems to have problems with this when my demo is running in the preview window, writing code slows down and becomes a pain.

To fix this, I'm going to add a “pause” button to my mind map demo that I can use to start/stop the rendering loop.

Pause button

Picking a State Management Library

I know I'm going to have to add some way of managing application state to my React app at some point. My plan is to ultimately have a web app where users log in to collaborate on mind maps with others.

My “pause” button is the first use case that actually requires an application state, so it's time to think about state management.

I've been using Redux in my day job for five years now. Some people think it's overly complicated. I disagree, especially with the latest version of Redux Toolkit, it's become a lot easier to adopt and use.

For small projects, I also like Zustand a lot – much more light-weight than Redux.

Then Recoil popped up this year and looks really promising, especially considering it is backed by Facebook, the company behind React.

However, for my collaborative mind mapping project, I've decided to go with Apollo Client.

This is much more than just a state management library. Apollo is a framework for storing and fetching data with GraphQL, an API query language.

I will need to store my users' mind map data in a database. GraphQL is a great way to access this stored data that I've been wanting to try out for a long time now.

Toggling my 3D animation loop on and off can, of course, be achieved much, much easier with far less overhead. Sooner or later, though, it is going to pay off to have one framework for managing my local application state and remote data storage.

Cracking a nut with a sledgehammer

So let's play Rube Goldberg and crack a nut with a sledgehammer – let's toggle a single boolean value with Apollo Client!

Recommended Reading

I'm not going to detail out every single step in today's blog post, I don't want to bore you to death. That being said, if you want to use Appollo Client for local state managment in your own app, I highly recommend this article:

This was published by the makers of Apollo and is the most up-to-date and comprehensive tutorial I've found.

When you google “apollo local state management”, you'll come across quite a few more – I found that they were almost all outdated.

There is a library apollo-link-state that is deprecated now, because Apollo Client now supports managing local state out of the box.

The Code

ApolloProvider

After adding the npm packages @apollo/client and graphql to my project, the first step is to initialize the client and add an ApolloProvider component:

index.js

import React from 'react';
import ReactDOM from 'react-dom';
import { ApolloClient, ApolloProvider } from '@apollo/client';
import { cache } from './storage';
import App from './App';

const client = new ApolloClient({
  cache
});

const rootElement = document.getElementById('root');
ReactDOM.render(
  <React.StrictMode>
    <ApolloProvider client={client}>
      <App />
    </ApolloProvider>
  </React.StrictMode>,
  rootElement
);
Enter fullscreen mode Exit fullscreen mode

Cache

Notice how the client in the code example above is initialized with a cache. The cache, in Apollo Client, is the central module for managing data:

cache.ts

import { InMemoryCache } from '@apollo/client';
import { appConfigVar } from './appConfig';

const cache: InMemoryCache = new InMemoryCache({
  typePolicies: {
    Query: {
      fields: {
        appConfig: {
          read() {
            return appConfigVar();
          }
        }
      }
    }
  }
});

export default cache;
Enter fullscreen mode Exit fullscreen mode

Note how the cache module is written in TypeScript. So far, I've been using JavaScript for my project. The tutorial I'm following along is written in TypeScript. I was planning to convert to TypeScript at some point, anyway, so I decide to use TypeScript for the Apollo modules in my project.

Reactive Variables

The appConfigVar in my cache is a reactive variable. This is where the magic happens – the idea is to create reactive variables for everything that is stored locally, i.e. not through a GraphQL API.

appConfigVar.ts

import { makeVar } from '@apollo/client';
import AppConfig from './AppConfig';
import initialAppConfig from './initialAppConfig';

const appConfigVar = makeVar<AppConfig>(initialAppConfig);

export default appConfigVar;
Enter fullscreen mode Exit fullscreen mode

AppConfig Interface

In TypeScript, we define types or interfaces to help the compiler check if everything is typed correctly.

AppConfig.ts

interface AppConfig {
  isPaused: boolean;
}

export default AppConfig;
Enter fullscreen mode Exit fullscreen mode

As you can see, I'm using AppConfig objects to store the state of my application. So far, this contains only one boolean value, isPaused. The nut I'm cracking with my sledgehammer.

Initial App Config

In appConfigVar.ts, I'm using initalAppConfig to set up my reactive variable.

initialAppConfig

import AppConfig from './AppConfig';

const initialAppConfig: AppConfig = JSON.parse(
  window.localStorage.getItem('nuffshell.appConfig')
) || {
  isPaused: false
};

export default initialAppConfig;
Enter fullscreen mode Exit fullscreen mode

I want my app state to be persistent, even when I reload the page in the browser. To achieve that, I'm storing it in the browser's localStorage.

When the app config reactive var is initialized, I'm checking the local storage for a previously saved app config. If there is one, I'm using this, otherwise, I use a default one, with isPaused: false.

Query to Get the App Config

To get the app config, I define a GraphQL query:

GetAppConfig.ts

import { gql } from '@apollo/client';

const GetAppConfig = gql`
  query GetAppConfig {
    appConfig @client {
      isPaused
    }
  }
`;

export default GetAppConfig;
Enter fullscreen mode Exit fullscreen mode

Notice the @client part in the query definition – this tells Apollo Client that the app config comes from a local state, i.e. it does not have to be fetched through the GraphQL API.

Custom Hook

I've decided to write a custom hook to wrap up all that Apollo goodness and to be able to conveniently use it from my React components:

useAppConfig.ts

import { useQuery } from '@apollo/client';
import appConfigVar from './appConfigVar';
import GetAppConfig from './GetAppConfig';
import saveAppConfig from './saveAppConfig';

export default function useAppConfig() {
  const {
    data: { appConfig }
  } = useQuery(GetAppConfig);

  return {
    isPaused: appConfig.isPaused,
    togglePause() {
      appConfigVar({ ...appConfig, isPaused: !appConfig.isPaused });
      saveAppConfig();
    }
  };
}
Enter fullscreen mode Exit fullscreen mode

As you can see, I'm using the useQuery hook from the Apollo Client to get the current value of the app config by passing my GetAppConfig query.

My hook returns an object that allows React components to get the current state (is the app paused?) and toggle the pause on/off.

Persisting the Config in Local Storage

In my custom hook, I'm calling this function saveAppConfig to store my config in the browser's local storage:

import appConfigVar from './appConfigVar';

export default function saveAppConfig() {
  window.localStorage.setItem(
    'nuffshell.appConfig',
    JSON.stringify(appConfigVar())
  );
}
Enter fullscreen mode Exit fullscreen mode

Toggle Button Component

Here's the useAppConfig in action, in the PauseButton component:

import React from 'react';
import { useAppConfig } from '../../storage/appConfig';
import styles from './PauseButton.module.css';

export default function PauseButton() {
  const { isPaused, togglePause } = useAppConfig();

  return (
    <button className={styles.PauseButton} onClick={togglePause}>
      {isPaused ? 'unpause' : 'pause'}
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode

Adding the Button

I'm adding this button component to my App component:

import React, { createRef, useEffect } from 'react';
import { PauseButton } from './features/pauseButton';
import renderMindMap from './renderMindMap';

export default function App() {
  const divRef = createRef();
  useEffect(() => renderMindMap(divRef.current), [divRef]);
  return (
    <>
      <PauseButton />
      <div ref={divRef} />
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Pausing the 3D Rendering

Finally, inside the loop that runs 60 times per second to re-render my 3D model, I can get the current state, asking: Is the app paused?

renderMindMap.js

(function animate() {
  const { isPaused } = appConfigVar();
  if (!isPaused) {
    graph.tickFrame();
    controls.update();
    renderer.render(scene, camera);
  }
  requestAnimationFrame(animate);
})();
Enter fullscreen mode Exit fullscreen mode

In this case, I'm not using the useAppConfig hook, because this is not a React component. I can simply get the app config by calling my reactive variable appConfigVar.

The Result

Wow, what a wild ride – so much code for such a little thing to achieve! Even old-school Redux is simpler. I hope it will be worth it in the long run, when I fetch and write user data through a GraphQL API and can then handle everything through Apollo.

To Be Continued…

I'm planning to turn my mind map into a social media network and collaboration tool and will continue to blog about my progress in follow-up articles. Stay tuned!

Discussion (0)