Did you know you can read to and write from your state using GraphQL? Apollo Link State allows you manage both local and remote data in a single place.
You don’t need a GraphQL API in order to use GraphQL. That may seem like a crazy statement, but with Apollo GraphQL you’re able to use apollo-link-state in order to read from and write to your state using queries and mutations.
In this article we’ll explore how to use apollo-link-state as a state management library to replace component state, Redux, or MobX. The topics we’ll be covering are as follows:
- How to set up link state and define resolvers and default data
- How to execute mutations that update the state
- How to execute queries to read the state
If at any point you get lost, the final version of this code can be found on GitHub.
Apollo and Data
Apollo keeps all of your data in a cache. Think of it like a normal data store that you’d have in MobX or Redux. Normally this cache gets populated by executing GraphQL queries, which puts the data that is returned from the server into the cache. You can read from or write to that cache on your own using the apollo-link-state
module.
Why would you do this? It allows you to execute GraphQL queries (or mutations) in your React code to access your state, without the need to have a GraphQL API. It also gives you a common language to refer to all forms of data, whether it be state or data that comes from a GraphQL API.
In our example, we’ll be adding a simple control that lets us choose how many starred repositories we’d like to see from the GitHub GraphQL API. Let’s see how it works!
Using Link State to Maintain State
The first step is to add the library: yarn add apollo-link-state
. After that, we’ll define some mutation resolvers. These resolvers allow us to look for special queries or mutations and deal with them locally rather than having them make a request to an external GraphQL API.
Below is the apolloClient.js
file where we define the Apollo Client along with all of the different links it is comprised of. If you are not familiar with the Apollo Client, please refer to the previous article in this series which focuses on basic Apollo Client setup. I have commented some of the links out to keep this code from growing too large.
// src/apolloClient.js
import { InMemoryCache } from "apollo-cache-inmemory";
import { ApolloClient } from "apollo-client";
import { ApolloLink } from "apollo-link";
import { HttpLink } from "apollo-link-http";
import { setContext } from "apollo-link-context";
import { onError } from "apollo-link-error";
import { withClientState } from "apollo-link-state";
const cache = new InMemoryCache();
const stateLink = withClientState({
cache,
resolvers: {
Mutation: {
updateStarredControls: (_, { numRepos }, { cache }) => {
const data = {
starredControls: {
__typename: "StarredControls",
numRepos
}
};
cache.writeData({ data });
return null;
}
}
},
defaults: {
starredControls: {
__typename: "StarredControls",
numRepos: 20
}
}
});
// errorLink defined here
// authLink defined here
// httpLink defined here
const client = new ApolloClient({
link: ApolloLink.from([errorLink, stateLink, authLink, httpLink]),
cache
});
export default client;
The main things you’ll pass to the withClientState
functions are:
-
cache
: Where the state will be stored. -
resolvers
: Any Mutations or Queries required to modify or read from the state (Queries aren’t an absolute necessity, though, if your query simply requests the data in the exact format it is stored in in the cache). -
defaults
: Default state values for the state you plan to have.
The resolver function we defined as updateStarredControls
looks like this:
(_, { numRepos }, { cache }) => {
const data = {
starredControls: {
__typename: "StarredControls",
numRepos
}
};
cache.writeData({ data });
return data;
};
Its main purpose is to receive the mutation variables { numRepos }
along with the { cache }
, and, using those two things, update the data within the cache. Refer to the Apollo docs for details about the resolver signature.
It’s important to note that the data you add to the cache must have a __typename
field, which helps Apollo normalize and store the data correctly.
Cleaning up Resolvers & Defaults
Just like in Redux, you can imagine that as your state grows, the amount of Mutation and Query resolvers you have will get unwieldy to contain in a single file. I recommend creating a resolvers
folder that contains a single file per related group of resolvers and defaults, with an index.js
file that merges them all together. For example:
// src/resolvers/starredControls.js
const resolvers = {
Mutation: {
updateStarredControls: (_, { numRepos }, { cache }) => {
const data = {
starredControls: {
__typename: "StarredControls",
numRepos
}
};
cache.writeData({ data });
return null;
}
}
};
const defaults = {
starredControls: {
__typename: "StarredControls",
numRepos: 20
}
};
export default { resolvers, defaults };
And then the code to merge them:
// src/resolvers/index.js
import merge from "lodash.merge";
import starredControls from "./starredControls";
export default merge(starredControls);
Which will allow us to update the definition of our stateLink
to be quite a bit shorter:
// src/apolloClient.js
import resolversDefaults from "./resolvers";
const stateLink = withClientState({
cache,
resolvers: resolversDefaults.resolvers,
defaults: resolversDefaults.defaults
});
Mutating State
To call the mutation resolver that we’ve defined, we’ll create a component called StarredControls
, which will give the user three options to choose from: Viewing 10, 20, or 30 repositories at a time. In the onClick
event, we’ll call the mutation function, passing the variables required (numRepos
). Below the code example, we’ll dive into a little more detail about how it works.
// src/StarredControls.js
import React, { Fragment } from "react";
import { Mutation } from "react-apollo";
import gql from "graphql-tag";
import { Button } from "@progress/kendo-react-buttons";
import styled from "styled-components";
// Just adding some additional styles to our kendo button
const Control = styled(Button)`
margin-right: 5px;
`;
// Defining the mutation to be executed
const UPDATE_STARRED_CONTROLS = gql`
mutation UpdateStarredControls($numRepos: Int!) {
updateStarredControls(numRepos: $numRepos) @client
}
`;
const StarredControls = ({ numRepos }) => (
<div>
View{" "}
<Mutation mutation={UPDATE_STARRED_CONTROLS}>
{updateStarredControls => (
<Fragment>
{[10, 20, 30].map(num => (
<Control
onClick={() => {
updateStarredControls({ variables: { num } });
}}
key={num}
>
{numRepos === num ? <strong>{num}</strong> : num}
</Control>
))}
</Fragment>
)}
</Mutation>{" "}
Repos
</div>
);
export default StarredControls;
First things first, aside from using styled-components to add some margin to the button we’re using from the Kendo UI Button library, you’ll see the definition of the mutation.
const UPDATE_STARRED_CONTROLS = gql`
mutation UpdateStarredControls($numRepos: Int!) {
updateStarredControls(numRepos: $numRepos) @client
}
`;
This looks like a normal mutation other than one key difference: The @client
directive. This signals to Apollo that it’s meant to use the resolvers we’ve defined for our state link.
The actual code to embed the Mutation
component into our React code is no different than it would be to execute a GraphQL mutation on a server API versus the client-side mutation we are executing.
Querying State
The query definition will look very familiar to normal query definitions in Apollo, with the main difference being the @client
directive (similar to what we did with the mutation), as a way of indicating to Apollo that this query is meant to read data from state.
Because our query matches how the data is stored and without any query variables that might modify the result, we didn’t even have to define a query resolver when we defined link-state, as Apollo is smart enough to read it from the data cache. This is called a default resolver.
// within src/StarredRepos.js
const STARRED_CONTROLS_QUERY = gql`
query StarredControlsQuery {
starredControls @client {
numRepos
}
}
`;
Once we’ve defined the query, using it is identical to how you would execute any other query within Apollo, be it to a GraphQL server or to state.
export default class StarredRepos extends React.Component {
render() {
return (
<div>
<Query query={STARRED_CONTROLS_QUERY}>
{({ data: { starredControls } }) => (
<Fragment>
<Status />
<StarredControls numRepos={starredControls.numRepos} />
<Query
query={STARRED_REPOS_QUERY}
variables={{ numRepos: starredControls.numRepos }}
>
{({ data: repoData, loading }) => {
if (loading) {
return <span>Loading...</span>;
}
return repoData.viewer.starredRepositories.nodes.map(node => (
<Repository data={node} key={node.id} />
));
}}
</Query>
</Fragment>
)}
</Query>
</div>
);
}
}
Conclusion
If I’m being honest, I see pros and cons to the idea of link-state. The pros to me are that you can think about querying/mutating your data in a single way, treating data from your state, data from the server, and potentially data from other sources (like a REST API) in the exact same way.
The cons to me are that managing state seems to be done more concisely and clearly using an existing state management library such as Redux, MobX, or simply sticking with component state for simple cases.
For more info on building apps with React:
- Check out our All Things React page that has a great collection of info and pointers to React information—with hot topics and up-to-date info ranging from getting started to creating a compelling UI.
- You can also learn more about KendoReact, our native component library built specifically for React, and what it can do for you.
Top comments (0)