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

mattdionis

Matt Dionis

Posted on August 28, 2019

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
Enter fullscreen mode Exit fullscreen mode

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"));
Enter fullscreen mode Exit fullscreen mode

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!
}
Enter fullscreen mode Exit fullscreen mode

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

type Query {
  appBarColorSetting: AppBarColorSetting!
}
Enter fullscreen mode Exit fullscreen mode

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

type Mutation {
  updateAppBarColorSetting(setting: String!): AppBarColorSetting!
}
Enter fullscreen mode Exit fullscreen mode

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
});
Enter fullscreen mode Exit fullscreen mode

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>
);
Enter fullscreen mode Exit fullscreen mode

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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"
  }
/>
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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
      });
    }
  }
);
Enter fullscreen mode Exit fullscreen mode

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!

💖 💪 🙅 🚩
mattdionis
Matt Dionis

Posted on August 28, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related