How to use Apollo‘s brand new Query components to manage local state

One of the biggest challenges in frontend web development is managing application state in a clean, predictable way. As apps grow in size and complexity, figuring out where to store data, how to update it, and how to efficiently retrieve it across many components becomes difficult.

While libraries like Redux and MobX have emerged as popular standalone solutions for state management, what if you‘re already using Apollo Client to fetch data from a GraphQL backend? Could you leverage the same tools to manage your app‘s local state as well?

The answer is yes! Apollo Client makes it easy to mix local and remote data in your app, thanks to its built-in local state handling capabilities. And with the addition of the new Query and Mutation components in Apollo Client 2.1, working with local data is even more declarative and intuitive.

In this post, we‘ll explore how to use Apollo‘s Query components to manage local state in a React app. We‘ll cover the key concepts and walk through practical examples of initializing a local data store, querying and updating local state, and seamlessly combining local and remote data. Let‘s jump in!

The case for managing local state in Apollo

So what‘s the benefit of using Apollo Client to manage local state, when tools like Redux already exist for that purpose? There are a few key advantages:

  1. Consistent tooling and concepts. By handling local state in Apollo, you can leverage the same Query and Mutation components, GraphQL syntax, and developer tools for both local and remote data. This keeps your app more consistent and reduces cognitive overhead.

  2. Easy combination of local and remote data. Apollo lets you request both local and remote fields in a single GraphQL query. You can even set up local resolvers that invoke server mutations. This allows for a fluid integration between client state and backend data.

  3. Less boilerplate. Compared to Redux, Apollo‘s local state tools cut down on the amount of setup code, action types, reducers, and selectors you need to write. The Query and Mutation components declaratively bind data and actions to your UI.

  4. Automatic reactive updates. Like any other Apollo query, your UI will automatically re-render as soon as any local state changes occur. There‘s no need to manually subscribe to a store or map state to props.

So if you‘re already comfortable working with Apollo and GraphQL, managing local state within the same ecosystem can be a big win for productivity and maintainability. With that said, let‘s see how it works in practice!

Configuring Apollo Client for local state

To get started with local state in Apollo, we first need to configure our Apollo Client instance. When creating the client, we‘ll specify a cache (typically InMemoryCache) and any local resolvers.

Here‘s a basic example of setting up Apollo Client with local state capabilities:

import ApolloClient from ‘apollo-client‘;
import { InMemoryCache } from ‘apollo-cache-inmemory‘;
import { ApolloLink } from ‘apollo-link‘;
import { withClientState } from ‘apollo-link-state‘;

const cache = new InMemoryCache();

const stateLink = withClientState({
  cache,
  resolvers: {
    Mutation: {
      updateNetworkStatus: (_, { isConnected }, { cache }) => {
        const data = {
          networkStatus: { isConnected },
        };
        cache.writeData({ data });
        return null;
      },
    },
  },
  defaults: {
    networkStatus: {
      __typename: ‘NetworkStatus‘,
      isConnected: true,
    },
  },
});

const client = new ApolloClient({
  cache,
  link: ApolloLink.from([stateLink]),
});

Let‘s break this down:

  1. We create an instance of InMemoryCache to serve as our local store.

  2. We create a state link using withClientState and pass it our cache, any local resolvers, and initial default state.

  3. The resolvers object defines our local mutations, which encapsulate the logic for updating the cache. Here we have one mutation updateNetworkStatus which writes a networkStatus object to the cache.

  4. In defaults, we specify any initial values we want to write to the cache when the client is created. Here we initialize networkStatus.isConnected to true.

  5. Finally, we instantiate our ApolloClient and pass it our cache and state link.

With our Apollo Client instance set up, we‘re ready to start querying and modifying local state within our components.

Querying local state

To query local state, we use the @client directive in our GraphQL queries to indicate any local fields. We can then bind the query to our UI using the Query component, just like we would for a remote query.

Here‘s an example of querying the networkStatus from our local cache:

import React from ‘react‘;
import { Query } from ‘react-apollo‘;
import gql from ‘graphql-tag‘;

const GET_NETWORK_STATUS = gql`
  query GetNetworkStatus {
    networkStatus @client {
      isConnected
    }
  }
`;

const NetworkStatus = () => (
  <Query query={GET_NETWORK_STATUS}>
    {({ data: { networkStatus } }) => (
      <div>
        <p>Network status: {networkStatus.isConnected ? ‘✅ Online‘ : ‘❌ Offline‘}</p>
      </div>
    )}
  </Query>
);

export default NetworkStatus;

In this example:

  1. We define our GraphQL query GET_NETWORK_STATUS which requests the isConnected field from the networkStatus object. The @client directive tells Apollo to resolve this from the local cache.

  2. We wrap our component with the Query component and pass our local query.

  3. The Query component‘s render prop function gives us the result data, from which we destructure networkStatus. We can then render this in our JSX.

Now whenever the networkStatus in the cache changes (e.g. if we call updateNetworkStatus), our component will reactively re-render with the latest data. We can use this technique to query any local state we‘ve written to our Apollo cache.

Mutating local state

To update local state, we define a GraphQL mutation and trigger it from our UI using the Mutation component. Let‘s expand our previous example to include a mutation for toggling the network status:

const UPDATE_NETWORK_STATUS = gql`
  mutation UpdateNetworkStatus($isConnected: Boolean) {
    updateNetworkStatus(isConnected: $isConnected) @client
  }
`;

const NetworkStatus = () => (
  <Query query={GET_NETWORK_STATUS}>
    {({ data: { networkStatus } }) => (
      <Mutation mutation={UPDATE_NETWORK_STATUS}>
        {updateNetworkStatus => (
          <div>
            <p>Network status: {networkStatus.isConnected ? ‘✅ Online‘ : ‘❌ Offline‘}</p>
            <button
              onClick={() => updateNetworkStatus({ variables: { isConnected: !networkStatus.isConnected } })}
            >
              Toggle Status
            </button>
          </div>
        )}  
      </Mutation>
    )}
  </Query>
);

Here‘s what‘s happening:

  1. We define an UPDATE_NETWORK_STATUS mutation which invokes the updateNetworkStatus resolver we defined in our initial client setup. This resolver takes an $isConnected argument.

  2. We wrap the mutation around the section of our component where we want to trigger the state change. The Mutation component‘s render prop gives us a mutate function, here named updateNetworkStatus.

  3. We pass this mutate function to our button‘s onClick handler. When called, it invokes the mutation with a variable containing the inverted isConnected status.

  4. Our updateNetworkStatus resolver writes this updated status to the cache.

  5. Since our component is wrapped in a Query for that same status, it reactively re-renders to reflect the new state of networkStatus.isConnected.

By modeling both reads and writes to our local state as GraphQL queries and mutations, we keep our component logic declarative and concise. The Apollo cache acts as our single source of truth, and takes care of the plumbing to keep the UI in sync.

Combining local and remote data

One of the most powerful features of state management with Apollo is the ability to seamlessly merge local and remote data in a single query. Simply include both local and remote fields in your query, each with the appropriate @client or @rest directives.

For example, we could combine our local networkStatus with a remote list of users:

const USERS_QUERY = gql`
  query UsersQuery {
    networkStatus @client {
      isConnected
    }
    users @rest(type: "User", path: "/users") {
      id
      name
    }
  }  
`;

const Users = () => (
  <Query query={USERS_QUERY}>
    {({ data: { networkStatus, users } }) => (
      <div>
        <p>Network: {networkStatus.isConnected ? ‘Online‘ : ‘Offline‘}</p>
        <ul>
          {users.map(user => <li key={user.id}>{user.name}</li>)}
        </ul>
      </div>
    )}
  </Query>
);

This query will fetch users from our REST endpoint, while retrieving networkStatus from the local cache. Our component receives both sets of data together, and can display them side by side. Apollo takes care of handling the different data sources and stitching the results back together into one convenient object.

This opens up lots of possibilities for progressively managing more state on the client side as needed, while still interacting with a remote GraphQL server. You can even set up local resolvers that "shadow" and extend server-side mutations and queries.

Conclusion

For applications already leveraging GraphQL and Apollo Client, using Apollo to manage local application state provides a flexible, intuitive way to model client-side data as a graph. It enables a fluid combination of local and remote data, and takes advantage of the familiar GraphQL and Apollo ecosystem.

With the addition of the Query and Mutation components, working with local state in Apollo is more declarative than ever. These components cut down on boilerplate while making data and state easy to reason about within your UI.

To review, here are the key steps for managing local state with Apollo Client:

  1. Configure your Apollo Client instance with an InMemoryCache, local resolvers, and initial default state
  2. Use the @client directive to query local data with the Query component
  3. Define local mutations and call them with the Mutation component to update the cache
  4. Combine local and remote queries as needed to retrieve a flexible mix of client and server state

Whether you‘re adding a few local fields to an existing app or building an entirely client-side application, Apollo‘s state management provides a cohesive set of tools for modeling and interacting with your data as a graph. Give it a try in your next project!

Similar Posts