DEV Community

Cover image for Move Over Redux: Apollo-Client as a State Management Solution (with Hooks 🎉)
Matt Dionis
Matt Dionis

Posted on

Move Over Redux: Apollo-Client as a State Management Solution (with Hooks 🎉)

Background

On the Internal Tools team at Circle, we recently modernized a legacy PHP app by introducing React components. Just a handful of months after this initiative began we have close to one-hundred React components in this app! 😲

We recently reached a point where we found ourselves reaching for a state management solution. Note that it took many months and dozens of components before we reached this point. State management is often a tool that teams reach for well before they need it. While integrating a state management solution into an application no doubt comes with many benefits it also introduces complexity so don’t reach for it until you truly need it.

Speaking of complexity, one complaint about the typical “go-to” state management solution, Redux, is that it requires too much boilerplate and can be difficult to hit-the-ground-running with. In this post, we will look at a more lightweight solution which comes with the added benefit of providing some basic GraphQL experience for those who choose to use it.

On the Circle 🛠 team, we know that our future stack includes GraphQL. In fact, in the ideal scenario, we would have a company-wide data graph at some point and access and mutate data consistently through GraphQL. However, in the short-term, we were simply looking for a low-friction way to introduce GraphQL to a piece of the stack and allow developers to wrap their heads around this technology in a low-stress way. GraphQL as a client-side state management solution using libraries such as apollo-client felt like the perfect way to get started. Let’s take a look at the high-level implementation of a proof-of-concept for this approach!

Configuring the client

First, there are a number of packages we’ll need to pull in:

yarn add @apollo/react-hooks apollo-cache-inmemory
apollo-client graphql graphql-tag react react-dom

Below you’ll find index.js on the client in its entirety. We’ll walk through the client-side schema specific pieces next:

import React from "react";
import ReactDOM from "react-dom";

import gql from "graphql-tag";
import { ApolloClient } from "apollo-client";
import { ApolloProvider } from "@apollo/react-hooks";
import { InMemoryCache } from "apollo-cache-inmemory";

import App from "./App";
import userSettings from "./userSettings";

const typeDefs = gql`
  type AppBarColorSetting {
    id: Int!
    name: String!
    setting: String!
  }
  type Query {
    appBarColorSetting: AppBarColorSetting!
  }
  type Mutation {
    updateAppBarColorSetting(setting: String!): AppBarColorSetting!
  }
`;

const resolvers = {
  Query: {
    appBarColorSetting: () => userSettings.appBarColorSetting
  },
  Mutation: {
    updateAppBarColorSetting: (_, { setting }) => {
      userSettings.appBarColorSetting.setting = setting;
      return userSettings.appBarColorSetting;
    }
  }
};

const client = new ApolloClient({
  cache: new InMemoryCache({
    freezeResults: true
  }),
  typeDefs,
  resolvers,
  assumeImmutableResults: true
});

const TogglesApp = () => (
  <ApolloProvider client={client}>
    <App />
  </ApolloProvider>
);

ReactDOM.render(<TogglesApp />, document.getElementById("root"));

First, we define typeDefs and resolvers.

The AppBarColorSetting type will have required id, name, and setting fields. This will allow us to fetch and mutate the app bar’s color through GraphQL queries and mutations!

type AppBarColorSetting {
  id: Int!
  name: String!
  setting: String!
}

Next up, we define the Query type so that we can fetch the appBarColorSetting:

type Query {
  appBarColorSetting: AppBarColorSetting!
}

Finally, you guessed it, we need to define the Mutation type so that we can update appBarColorSetting:

type Mutation {
  updateAppBarColorSetting(setting: String!): AppBarColorSetting!
}

Finally, we set up our client. Often, you will find yourself instantiating ApolloClient with a link property. However, since we have added a cache and resolvers, we do not need to add a link. We do, however, add a couple of properties that may look unfamiliar. As of apollo-client 2.6, you can set an assumeImmutableResults property to true to let apollo-client know that you are confident you are not modifying cache result objects. This can, potentially, unlock substantial performance improvements. To enforce immutability, you can also add the freezeResults property to inMemoryCache and set it to true. Mutating frozen objects will now throw a helpful exception in strict mode in non-production environments. To learn more, read the “What’s new in Apollo Client 2.6” post from Ben Newman.

const client = new ApolloClient({
  cache: new InMemoryCache({
    freezeResults: true
  }),
  typeDefs,
  resolvers,
  assumeImmutableResults: true
});

That’s it! Now, simply pass this client to ApolloProvider and we’ll be ready to write our query and mutation! 🚀

const TogglesApp = () => (
  <ApolloProvider client={client}>
    <App />
  </ApolloProvider>
);

Querying client-side data

We’re now going to query our client cache using GraphQL. Note that in this proof-of-concept, we simply define the initial state of our userSettings in a JSON blob:

{
  "appBarColorSetting": {
    "id": 1,
    "name": "App Bar Color",
    "setting": "primary",
    "__typename": "AppBarColorSetting"
  }
}

Note the need to define the type with the __typename property.

We then define our query in its own .js file. You could choose to define this in the same file the query is called from or even in a .graphql file though.

import gql from "graphql-tag";

const APP_BAR_COLOR_SETTING_QUERY = gql`
  query appBarColorSetting {
    appBarColorSetting @client {
      id
      name
      setting
    }
  }
`;

export default APP_BAR_COLOR_SETTING_QUERY;

The most important thing to notice about this query is the use of the @client directive. We simply need to add this to the appBarColorSetting query as it is client-specific. Let’s take a look at how we call this query next:

import React from "react";
import { useQuery } from "@apollo/react-hooks";

import AppBar from "@material-ui/core/AppBar";
import Toolbar from "@material-ui/core/Toolbar";
import Typography from "@material-ui/core/Typography";
import IconButton from "@material-ui/core/IconButton";
import MenuIcon from "@material-ui/icons/Menu";

import SettingsComponent from "./components/SettingsComponent";
import APP_BAR_COLOR_SETTING_QUERY from "./graphql/APP_BAR_COLOR_SETTING_QUERY";

function App() {
  const { loading, data } = useQuery(APP_BAR_COLOR_SETTING_QUERY);

  if (loading) return <h2>Loading...</h2>;
  return (
    <div>
      <AppBar position="static" color={data.appBarColorSetting.setting}>
        <Toolbar>
          <IconButton color="inherit" aria-label="Menu">
            <MenuIcon />
          </IconButton>
          <Typography variant="h6" color="inherit">
            State Management with Apollo
          </Typography>
        </Toolbar>
      </AppBar>
      <SettingsComponent
        setting={
          data.appBarColorSetting.setting === "primary"
            ? "secondary"
            : "primary"
        }
      />
    </div>
  );
}

export default App;

Note: we are using Material-UI in this app, but obviously the UI framework choice is up to you. 🤷‍♂️

const { loading, data } = useQuery(APP_BAR_COLOR_SETTING_QUERY);

We show a basic loading indicator and then render the app bar with data.appBarColorSetting.setting passed into the color attribute. If you are using the Apollo Client Developer Tools, you’ll be able to clearly see this data sitting in the cache.

Apollo State Management example screenshot

Mutating client-side data and updating the cache

You may have noticed this block of code in our App component. This simply alternates the value of setting based on its current value and passes it to our SettingsComponent. We will take a look at this component and how it triggers a GraphQL mutation next.

<SettingsComponent
  setting={
    data.appBarColorSetting.setting === "primary" ? "secondary" : "primary"
  }
/>

First, let’s take a peek at our mutation:

import gql from "graphql-tag";

const UPDATE_APP_BAR_COLOR_SETTING_MUTATION = gql`
  mutation updateAppBarColorSetting($setting: String!) {
    updateAppBarColorSetting(setting: $setting) @client
  }
`;

export default UPDATE_APP_BAR_COLOR_SETTING_MUTATION;

Again, notice the use of the @client directive for our client-side updateAppBarColorSetting mutation. This mutation is very simple: pass in a required setting string and update the setting.

Below you will find all the code within our SettingsComponent which utilizes this mutation:

import React from "react";
import { useMutation } from "@apollo/react-hooks";

import Button from "@material-ui/core/Button";

import UPDATE_APP_BAR_COLOR_SETTING_MUTATION from "../graphql/UPDATE_APP_BAR_COLOR_SETTING_MUTATION";
import APP_BAR_COLOR_SETTING_QUERY from "../graphql/APP_BAR_COLOR_SETTING_QUERY";

function SettingsComponent({ setting }) {
  const [updateUserSetting] = useMutation(
    UPDATE_APP_BAR_COLOR_SETTING_MUTATION,
    {
      variables: { setting },
      update: cache => {
        const data = cache.readQuery({
          query: APP_BAR_COLOR_SETTING_QUERY
        });

        const dataClone = {
          ...data,
          appBarColorSetting: {
            ...data.appBarColorSetting,
            setting
          }
        };

        cache.writeQuery({
          query: APP_BAR_COLOR_SETTING_QUERY,
          data: dataClone
        });
      }
    }
  );
  return (
    <div style={{ marginTop: "50px" }}>
      <Button variant="outlined" color="primary" onClick={updateUserSetting}>
        Change color
      </Button>
    </div>
  );
}

export default SettingsComponent;

The interesting piece of this code that we want to focus on is the following:

const [updateUserSetting] = useMutation(
  UPDATE_APP_BAR_COLOR_SETTING_MUTATION,
  {
    variables: { setting },
    update: cache => {
      const data = cache.readQuery({
        query: APP_BAR_COLOR_SETTING_QUERY
      });

      const dataClone = {
        ...data,
        appBarColorSetting: {
          ...data.appBarColorSetting,
          setting
        }
      };

      cache.writeQuery({
        query: APP_BAR_COLOR_SETTING_QUERY,
        data: dataClone
      });
    }
  }
);

Here, we make use of the apollo/react-hooks useMutation hook, pass it our mutation and variables, then update the cache within the update method. We first read the current results for the APP_BAR_COLOR_SETTING_QUERY from the cache then update appBarColorSetting.setting to the setting passed to this component as a prop, then write the updated appBarColorSetting back to APP_BAR_COLOR_SETTING_QUERY. Notice that we do not update the data object directly, but instead make a clone of it and update setting within the clone, then write the cloned data object back to the cache. This triggers our app bar to update with the new color! We are now utilizing apollo-client as a client-side state management solution! 🚀

Apollo State Management in action gif

Takeaways

If you’d like to dig into the code further, the CodeSandbox can be found here. This is admittedly a very contrived example but it shows how easy it can be to leverage apollo-client as a state management solution. This can be an excellent way to introduce GraphQL and the Apollo suite of libraries and tools to a team who has little to no GraphQL experience. Expanding use of GraphQL is simple once this basic infrastructure is in place.

I would love to hear thoughts and feedback from everyone and I hope you learned something useful through this post!

Top comments (4)

Collapse
 
troyschmidt profile image
Troy Schmidt

This is a very interesting proposal for handling state management. Are there any expected benefits from this approach versus Redux? Besides allowing the teams to get comfortable with GraphQL.
Also, with hook and concurrent mode coming it seems like more optimized and performant solutions than Redux would emerge. Specifically I am thinking of having a state management solution for the global application state pieces, but then the state that is only shared inside of modules could be handled with useReducer and ContextAPI.

Collapse
 
mattdionis profile image
Matt Dionis

Thanks. Great questions which were addressed a bit in this thread: twitter.com/swyx/status/1166733744...

It sounds like Apollo is looking to improve upon this existing solution to make state management even easier and more robust. With that said, several folks in that thread make great points about why this may not be the best approach.

Still an interesting exercise and I'm glad it started a conversation.

Collapse
 
richicoder1 profile image
Richard Simpson

I'm curious why you prefer writing to an in-memory object over the cache like the official docs recommend?

Collapse
 
mattdionis profile image
Matt Dionis

Great question. If this is in reference to the hard-coded JSON blob:

{
  "appBarColorSetting": {
    "id": 1,
    "name": "App Bar Color",
    "setting": "primary",
    "__typename": "AppBarColorSetting"
  }
}

...that was a choice I made just for this proof-of-concept and not something I'd actually suggest in a real-world use case.