Navigate back to the homepage

Wrestling with Apollo Local State and winning 🤼‍♂️

Alec Brunelle
May 16th, 2019 · 2 min read

Recently we bought into GraphQL and use it in every one of our web apps, both on the client and server level. It’s been helpful reducing unnecessary communication needed between our different teams when it comes to knowing what our many, many different API’s do. This contributes to our async work strategy and keeps developers moving and focusing on difficult problems versus organizational bloat.

For use with our frontend applications, we opted for Apollo Client with React which seems to be the one true GraphQL client at this point. As the library is fairly new (the javascript ecosystem moves fast, who knew?) we have experienced our fair share of pains and troubleshooting. Some of which included:

This article focuses on the first point, managing frontend local state. When looking for state management solutions, Apollo Local State (formally apollo-link-state) popped up. A couple of reasons led us to using this library:

  • The data models and store structure can be shared between the data fetching cache and the local state management cache
    • This leads to sharing of Typescript types as well 😉
  • Actions are performed with mutations, something that previous GraphQL users already understand.
  • Staying within the Apollo ecosystem meant smooth integration with existing tools, meaning less overhead for developers.
  • Backed by Apollo meant that support would be there for a significant amount of time.

Another good sign was this very attractive article which explains the advantages of keeping your local state close to the GraphQL schema vs using something like Redux.

Here comes the pain

A pattern established by Flux (the paradigm, not the library), has you splitting up actions for every event which happens in your app. User clicked a button, action is triggered, user scrolls down a certain length, action is triggered. Your app can observe these actions and manipulate the state accordingly. With Apollo Local State Mutations, this becomes much more intentional. No observability is given, each action is directly related to a resolver.

For example, you want to update a name on an issue (think GitHub just for examples sake) in the cache, this is Apollo’s term for state:

1const IssueContainer: React.FC<{ issue: GithubIssue }> = ({ issue }) => (
2 <UpdateIssueNameMutation mutation={UPDATE_ISSUE_NAME}>
3 {updateIssuename => (
4 <IssueFields
5 issue={issue}
6 onChange={name => {
7 updateIssuename({
8 variables: {
9 input: {
10 id: issue.id,
11 name
12 }
13 }
14 });
15 }}
16 />
17 )}
18 </UpdateIssueNameMutation>
19);

For this above mutation, here is an example of what we would need to write in Typescript, I break down what’s going on in the comments:

1import gql from "graphql-tag";
2import { IFieldResolver } from "graphql-tools";
3
4import { ISSUE_PARTS } from "../issues";
5import { IssueParts, UpdateIssueNameVariables } from "../../graphql-types";
6import { ResolverContext } from ".";
7
8export const UPDATE_ISSUE_NAME = gql`
9 mutation UpdateIssueName($input: UpdateIssueNameInput!) {
10 updateIssueName(input: $input) @client
11 }
12`;
13
14const ISSUE_FRAGMENT = gql`
15 ${ISSUE_PARTS}
16
17 fragment IssueParts on Issue {
18 id @client
19 name @client
20 }
21`;
22
23/**
24 * Updates the name of a Github issue.
25 **/
26const updateIssuename: IFieldResolver<void, ResolverContext, any> = (
27 _obj,
28 args: UpdateIssueNameVariables,
29 context
30) => {
31 const { input } = args;
32 const { cache, getCacheKey } = context;
33
34 // 1. Get the id of the object in the cache using the actual issue id
35 const id = getCacheKey({
36 __typename: "Issue",
37 id: input.id
38 });
39
40 // 2. Get the data from the cache
41 const issue: IssueParts | null = cache.readFragment({
42 fragment: ISSUE_FRAGMENT,
43 fragmentName: "IssueParts",
44 id
45 });
46 if (!issue) {
47 return null;
48 }
49
50 // 3. Update the data locally
51 const updatedIssue = {
52 ...issue,
53 name: input.name
54 };
55
56 // 4. Write the data back to the cache
57 cache.writeFragment({
58 fragment: ISSUE_FRAGMENT,
59 fragmentName: "IssueParts",
60 id,
61 data: updatedIssue
62 });
63 return null;
64};
65
66export default updateIssuename;

That’s 60 lines to update a single attribute on one data entity in the cache. After doing a couple of these, you will start to pull your hair out. Mutations as is, don’t do a whole lot for you, this results in a lot of boilerplate. Having all of the boilerplate code is not ideal, it leads to more bugs and thus more tests need to be written to avoid those bugs.

Looking for patterns

After writing about ten of these with plans to write a lot more, we wrote a small Yeoman generator to speed up the process. This made writing them a lot faster but didn’t solve the bloat in our codebase. Every mutation ended up doing the same thing as described in the comments above:

  1. Get the id of the object in the cache using the actual entity id
  2. Get the data from the cache
  3. Update the data locally
  4. Write the data back to the cache

The solution

Naturally, we wrote a helper which would help us refactor our resolvers.

1import { IFieldResolver } from "graphql-tools";
2import { DocumentNode } from "graphql";
3
4import { ResolverContext } from ".";
5
6interface InputVariablesShape<TInput> {
7 input: TInput;
8}
9
10/**
11 * Creates a client-side mutation resolver which reads one item from the cache,
12 * mutates it using the given mutation, then writes it back to the cache.
13 * @param reducer Func which mutates data and returns it. Must be a pure function.
14 * @param fragment Used to read/write data to apollo-link-state.
15 * @param fragmentName Name of fragment inside
16 * @param getId A func which returns the id of the entity to be used in the `reducer` func.
17 */
18export const createResolver = <InputShape, EntityType>(
19 reducer: (entity: EntityType, input: InputShape) => EntityType,
20 fragment: DocumentNode,
21 fragmentName: string,
22 getId: (input: InputShape) => string
23) => {
24 const resolver: IFieldResolver<void, ResolverContext, any> = (
25 _obj,
26 args: InputVariablesShape<InputShape>,
27 { cache, getCacheKey }
28 ) => {
29 const { input } = args;
30
31 // 1. Get the id of the object in the cache using `getId`
32 const id = getCacheKey({ id: getId(input) });
33
34 // 2. Get the data from the cache
35 const entity: EntityType | null = cache.readFragment({
36 fragment,
37 fragmentName,
38 id
39 });
40
41 if (!entity) {
42 return null;
43 }
44
45 // 3. Update the data locally
46 const newEntity: EntityType = reducer(entity, input);
47
48 // 4. Write the data back to the cache
49 cache.writeFragment({
50 fragment,
51 fragmentName,
52 id,
53 data: newEntity
54 });
55 return null;
56 };
57
58 return resolver;
59};

This resulted in the resolver code becoming a small pure func:

1const fragment = gql`
2 ${ISSUE_PARTS}
3
4 fragment BasicIssueParts on BasicIssue {
5 ... on Node {
6 id
7 }
8 parameters {
9 id
10 value
11 }
12 }
13`;
14
15export const reducer = (
16 issue: IssueParts,
17 input: UpdateIssueNameInput
18): IssueParts => ({
19 ...issue,
20 name: input.name
21});
22
23export default createResolver(
24 reducer,
25 fragment,
26 "IssueParts",
27 (input: UpdateIssueNameInput) => {
28 return input.id;
29 }
30);

A two-line resolver which does the same as before. This covered about 90% of our use-cases!

The road ahead

If you found this blog post helpful, don’t hesitate to steal the gist of this code.

Join our email list and get notified about new content

Be the first to receive our latest content with the ability to opt-out at anytime. We promise to not spam your inbox or share your email with any third parties.

More articles from Alec Brunelle

Hacking my Honeymoon with Javascript 🤣

How I used a little javascript to book The Giraffe Manor in Kenya

May 1st, 2019 · 2 min read

How Learning Elixir Made Me a Better Programmer 🥃

After getting comfortable with a couple programming technologies, developers usually stop there.

August 20th, 2018 · 5 min read
© 2017–2020 Alec Brunelle
Link to $https://twitter.com/yourboybigalLink to $https://github.com/aleccool213Link to $https://www.linkedin.com/in/alecbrunelle/Link to $https://unsplash.com/@aleccool21Link to $https://medium.com/@yourboybigalLink to $https://stackoverflow.com/users/3287767/aleccool21