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:
-
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.
-
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.
-
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.
-
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:
-
We create an instance of
InMemoryCache
to serve as our local store. -
We create a state link using
withClientState
and pass it our cache, any local resolvers, and initial default state. -
The resolvers object defines our local mutations, which encapsulate the logic for updating the cache. Here we have one mutation
updateNetworkStatus
which writes anetworkStatus
object to the cache. -
In
defaults
, we specify any initial values we want to write to the cache when the client is created. Here we initializenetworkStatus.isConnected
to true. -
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:
-
We define our GraphQL query
GET_NETWORK_STATUS
which requests theisConnected
field from thenetworkStatus
object. The@client
directive tells Apollo to resolve this from the local cache. -
We wrap our component with the
Query
component and pass our local query. -
The Query component‘s render prop function gives us the result
data
, from which we destructurenetworkStatus
. 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:
-
We define an
UPDATE_NETWORK_STATUS
mutation which invokes theupdateNetworkStatus
resolver we defined in our initial client setup. This resolver takes an$isConnected
argument. -
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 namedupdateNetworkStatus
. -
We pass this mutate function to our button‘s
onClick
handler. When called, it invokes the mutation with a variable containing the invertedisConnected
status. -
Our
updateNetworkStatus
resolver writes this updated status to the cache. -
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:
- Configure your Apollo Client instance with an InMemoryCache, local resolvers, and initial default state
- Use the
@client
directive to query local data with the Query component - Define local mutations and call them with the Mutation component to update the cache
- 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!