Apollo Client Tutorial
Learning Apollo Client 3

Learning Apollo Client 3.0 with non-abstract examples

Apollo Client is a powerful and flexible GraphQL front-end client library that can be used with React, Vue, Angular, and many others. It can also be used with just plain JavaScript. It offers many features like network communication, reactive updates, client-side data graph in-memory cache management, local app state management, and many others.

In this lesson, we’ll start with a React application that uses a GraphQL API without a GraphQL client. We’ll convert all of its GraphQL operations to use Apollo Client hook functions. We’ll see examples of queries, mutations, and subscriptions and we’ll also see how Apollo Client can replace the local app state management in that React application.

The React application we’ll use for this lesson is a simplified version of a project I am working on which I named AZdev (A to Z of development notes). AZdev is a searchable library of what software developers usually need to look up and a quick way for them to find concise approaches to handle exactly the tasks they need at the moment.

The home page of the project shows some examples of the tasks one would find on AZdev:

azdev home page
Figure 1. AZdev’s home page showing sample development data

You can certainly find approaches to these tasks by reading documentations and stack-overflow questions, but wouldn’t it be nice to have a site that features specific tasks like these with their exact approaches without all the noise? That’s AZdev.

azdev task page
Figure 2. An example of a Task page at AZdev

If you’re interested in updates about the AZdev project or if you’d like to get an invite when it’s ready (later this year), you can subscribe to the project email list at az.dev/join. You can also follow the project official GitHub repo at az.dev/contribute.

This lesson assumes that you’re comfortable with React so it will not explain any React concepts. The focus of this lesson is purely on how to use the Apollo Client methods in a React application. If you’re new to React, please start at jscomplete.com/learn-react.

This lesson features a decent-size React project with many files and features. Code modifications are often presented partially to focus on what has changed. I also explain a few examples and then leave the rest of what is similar for you to experiment with. However, all code changes are tracked in the lesson’s GitHub repo using git tags.

1. Setup and first examples

Start by cloning the application’s repo from GitHub:

The default "main" branch you’ll get when you clone the repo is the first version of this application before all the modifications that we will be doing in this lesson. There is also the "apollo" branch that has the final version of the code after all the modifications that we are making here. The repo also has five git tags (v1-v5) that you can checkout as we’re making progress through the lesson’s sections.

Install all the dependencies this repo needs:

cd apollo-client-3-example && npm install

1.1. Project structure

This repo comes with three main directories:

The db directory

This directory has a template.sql file which you can use to create the project’s PostgreSQL database and import the sample development data. Alternatively, you can use the docker.yml file there to spin up the Docker container image that I prepared for the project. That image is ready with the sample development data you see in the home page screenshot above.

Docker

Don’t be intimidated by Docker. I am only going to use it to download and run a prepared image. You don’t need to understand how it works to follow up with this lesson but I certainly recommend it. The big picture of Docker is that it uses your OS virtualization to provide software in packaged containers. It’s available on all 3 major operating systems although on Windows "Home" you’ll need a bit more work to get it to run.

To use the prepared Docker container image, You need to have Docker Compose in your path. This command is part of Docker Desktop) which is what I use.

Once Docker is running, you can use this command to download and run the project’s PostgreSQL database:

npm run db-server

If you can’t use Docker or if you’d rather use your own PostgreSQL database, you’ll need to execute the SQL in db/template.sql, which will create the project’s tables and load the sample development data.

The api directory

This directory has the back-end GraphQL API service implementation. We will not be doing any modifications for the API service in this lesson. You just need to run that server with the command:

npm run api-server

This runs the API server on port 4321.

GraphQL In Action

The full AZdev project is the focus of my GraphQL In Action book. The code presented in this lesson is a simplified version of it. In the GIA book, I build the back-end service step-by-step and explain it in depth. I also explain all the queries, mutations, and fragments that we’ll use in the front-end server.

The web directory

This is where the React project lives. This starts with a working state that uses direct Ajax calls to perform all GraphQL operations. This is where we’ll be introducing and learning about Apollo Client.

This project uses a simple Parcel configuration to bundle and server the React project assets. You can run it with the command:

npm run web-server

You can now test the project’s UI at http://localhost:1234. Get familiar with the UI and then take a look around the web directory and explore the React components it uses. The code uses React’s context to manage the local app state and issues both GraphQL queries and mutations using direct Ajax calls with "axios".

I’ve kept the code in this project as simple as possible and opted not to introduce advanced features like routing or server-side rendering. I’ve also skipped many optimizations to keep the examples short and focused. However, the project still has a practical list of features:

  • It’s a single-page app where all data is fetched with Ajax

  • Users can sign-up, login, and logout. A session token is stored in localStorage

  • The main page shows a list of Tasks

  • The Task page shows a list of Approaches on a Task and users can vote on them

  • Users can create new Tasks and optionally mark a Task as private

  • Users can see a list of their own Tasks

  • Users can search all Tasks and Approaches

  • A private Task only available for its owners. It does not show up in search unless the user who is searching is its owner

  • Users can add new Approaches to a Task

1.2. Making a query request

The first step to work with Apollo Client is to add it to the project dependencies. It’s hosted under the npm package "@apollo/client":

npm install @apollo/client

You can import a few things from the "@apollo/client" package. Let’s explore them with an example.

Delete all the content in web/src/index.js and replace it with the following:

Initializing and using Apollo Client
import "regenerator-runtime/runtime";
import {
  ApolloClient,
  HttpLink,
  InMemoryCache,
  gql,
} from "@apollo/client";

import { GRAPHQL_SERVER_URL } from "./config";

const cache = new InMemoryCache();
const httpLink = new HttpLink({ uri: GRAPHQL_SERVER_URL });
const client = new ApolloClient({ link: httpLink, cache });

async function main() {
  const resp = await client.query({
    query: gql`
      {
        taskList {
          content
        }
      }
    `,
  });
  console.log(resp.data);
}

main();

This is the simplest example for using Apollo Client in plain JavaScript but it already introduces many new concepts. Let’s walk through them:

  • A client library like Apollo replaces any other Ajax library in your application. You don’t need "axios" (or "fetch") to make Ajax requests because the client will make all the requests internally. This is the primary task of every GraphQL client out there. They make all the Ajax requests for you and abstract the complexities of dealing with HTTP requests and responses.

  • The ApolloClient object is a constructor that can be used to initialize a client object per GraphQL service. An application might use multiple clients to work with multiple GraphQL services. You can also use the same client object and change the link attribute when needed to maintain a single store for all of the application’s data. In this example, we have one httpLink object and I initialized it using the GRAPHQL_SERVER_URL which has the project’s backend server URL (http://localhost:4321).

  • In addition to the link attribute, Apollo Client requires the cache attribute. This attribute is used to specify the cache object that Apollo will use for its store. The default cache is an instance of the InMemoryCache object, which will make Apollo Client use the browser’s memory for caching. That’s what most web applications will need to do but the flexibility allows an Apollo Client instance to be used with other implementations of cache.

  • Once you have a client object initialized and configured with a valid GraphQL service link attribute and caching strategy, you can use its API methods. This example uses the query method to send a GraphQL query operation and retrieve the server response for it. The query method takes an object whose query property is an object representing the GraphQL operation text to be sent.

  • Instead of using a string with Apollo Client query method, we wrap a GraphQL operation string with the gql tag function. The gql function parses a GraphQL string into an AST (Abstract Syntax Tree). It basically converts the string into a structured object. Strings are limited. Structured objects give GraphQL clients more control over GraphQL operations and make it easier for them to offer advanced features.

The gql function also enables editors to show syntax highlighting and use tools like Prettier to auto-format the text of GraphQL operations. If you have Prettier configured in your editor, try to change the taskList string format and run Prettier again.

If everything works fine, you should see the console.log message in the browser’s console displaying the result of the taskList query:

task list test1
Figure 3. Output of the query request

At first glance, the above example is a lot of code to do a simple request that can be done with a direct Ajax call.. However, this code already comes with a huge win that we can see right away. Just repeat the same query operation again and look at the network tab in your browser when you refresh the session.

For this test, you can replace the main function’s definition in web/src/index.js with:

Repeating a query with Apollo Client
async function main() {
  const resp1 = await client.query({
    query: gql`
      {
        taskList {
          content
        }
      }
    `,
  });
  console.log(resp1.data);

  const resp2 = await client.query({
    query: gql`
      {
        taskList {
          content
        }
      }
    `,
  });
  console.log(resp2.data);
}

This is what you would see in the browser’s network tab, filtered to show only XHR requests (XMLHttpRequest):

task list test 2
Figure 4. Output of the repeated query request

Apollo Client issued only ONE Ajax request for both query operations because the response of the first request was automatically cached (in memory) and Apollo Client figured out that there is no need to go ask the server again for the same data that we already have.

This is a simple example, but Apollo Client does a lot of heavy lifting under the hood to make the cache as useful as it can be. For example, it caches every data response in a flattened data structure so that it can use the cache of individual objects to determine what future network requests are needed even for different queries. To see an example of that, make the first query ask for more fields:

Repeating a partial query
async function main() {
  const resp1 = await client.query({
    query: gql`
      {
        taskList {
          content
        }
      }
    `,
  });
  console.log(resp1.data);

  const resp2 = await client.query({
    query: gql`
      {
        taskList {
          content
        }
      }
    `,
  });
  console.log(resp2.data);
}

This code now asks to send two different query operations to the server. However, because the second one is a subset of the first one, Apollo Client would still not go to the server a second time! Verify that in the network tab.

Let me give you another example, but this time with a mutation operation.

1.3. Making a mutation request

To make a mutation request, you can use the .mutate method (instead of .query) and give it an object with a mutation property. If the operation to be sent uses variables, you can include a variables property to supply their values.

The following is an example of the mutation operation to vote on Approach #2. I’ve also included a query to fetch the voteCount field for Approach #2 (which is under Task #2) before and after the mutation to verify that it worked. Replace the main function’s definition in web/src/index.js with:

Sending a mutation request
async function main() {
  const resp1 = await client.query({
    query: gql`
      query taskInfo {
        taskInfo(id: "2") {
          approachList {
            id
            voteCount
          }
        }
      }
    `,
  });
  console.log(resp1.data);

  const resp2 = await client.mutate({
    mutation: gql`
      mutation approachVote($approachId: ID!) {
        approachVote(approachId: $approachId, input: { up: true }) {
          approach {
            id
            voteCount
          }
        }
      }
    `,
    variables: { approachId: "2" },
  });
  console.log(resp2.data);

  const resp3 = await client.query({
    query: gql`
      query taskInfo {
        taskInfo(id: "2") {
          approachList {
            id
            voteCount
          }
        }
      }
    `,
  });
  console.log(resp3.data);
}

The first query request should show that Approach #2 has 0 votes. The second request will update that vote count to 1 and the third will verify that:

mutation example
Figure 5. Output of the mutation request code

Note how Apollo Client included the introspective __typename field in all three operations although we did not specify it. How did it do that, and more importantly, why did it do that?

That’s one other reason why we’re wrapping all operations with gql. Since the requests are represented as objects, Apollo Client can inspect these objects and easily modify them to, for example, include the __typename field.

To understand why Apollo Client did that, take a good look at your network tab (or mine in the screenshot above) and note how Apollo Client made only 2 network requests. It did not issue a network request for the third query operation because that operation did not ask for anything new. The mutation operation already informed Apollo Client that the new voteCount is 1 so it cached that. Since it asked for the __typename as well, it can use a combination of __typename and id to uniquely identify each object it sees on a global level. It did exactly that for Approach #2. It used its globally-unique ID (Approach:2) to determine that we already know that object’s id and voteCount.

That’s impressive. We did nothing special to make this powerful feature work. We’re just issuing the same simple queries and mutations and Apollo Client is smartly making the cache seamlessly work for them.

With that, we’re ready to start converting the whole React project to use Apollo Client. We’ll first replace the manual Ajax requests the app is currenting doing with Apollo’s query and mutate methods. Then, we’ll explore how to invoke these operations with Apollo’s React-specific methods.

You can now revert the test changes we made so far in web/src/index.js and put the original code in there:

Code in web/src/index.js
import "regenerator-runtime/runtime";
import React from "react";
import ReactDOM from "react-dom";

import Root from "./components/root";

ReactDOM.render(<Root />, document.getElementById("root"));

2. Using Apollo Client with React

To use Apollo Client with React, we first need to initialize it (just like we did in the tests above). The best place to do so for this application is in web/src/store.js where the local app state is currently managed:

Changes in web/src/store.js
// ·-·-·

import {
  ApolloClient,
  HttpLink,
  InMemoryCache,
} from "@apollo/client";

const cache = new InMemoryCache();
const httpLink = new HttpLink({ uri: GRAPHQL_SERVER_URL });
const client = new ApolloClient({ link: httpLink, cache });

// ·-·-·

2.1. Using the query and mutate methods directly

Now, instead of the axios-based request method that is exported in the store’s context object, we need to introduce two new methods: one for queries and one for mutations. To keep the changes in the app to a minimum, we’ll start by having the exact same function signature as the good-old request function. The new methods will just be wrappers around Apollo Client’s methods:

Changes in web/src/store.js
export const useStore = () => {
  // ·-·-·

  const query = async (query, { variables } = {}) => {
    const resp = await client.query({ query, variables });
    return resp;
  };

  const mutate = async (mutation, { variables } = {}) => {
    const resp = await client.mutate({ mutation, variables });
    return resp;
  };

  return {
    getLocalAppState,
    setLocalAppState,
    AppLink,
    query,
    mutate,
  };
};
You can remove the request method and the "axios" dependency. We will no longer need that dependency.

Note how in the old request method we included the current user’s authToken in the headers of the Ajax request. We’ll also need to do that with Apollo and that’s why I kept the new query/mutate method wrappers within the useStore function. We’ll talk about how to include headers with Apollo in the next section. Later in the lesson, we’ll also see how Apollo Client can replace the entire local app state store.

Now for every call to the old request function in all components, we have to do the following:

  • Add an import { gql } from "@apollo/client" statement.

  • Wrap the query or mutation text with gql.

  • Instead of request, destructure query or mutate out of useActions.

  • Replace the request method with query if the operation is a query and mutate if the operation is a mutation.

For example, in web/src/components/home.js, here are the changes we have to make (in bold):

Changes in web/src/components/home.js
import { gql } from "@apollo/client";

const TASK_LIST = gql`
  query taskList {
    taskList {
      id
      ...TaskSummary
    }
  }

  ${NEED_SUMMARY_FRAGMENT}
`;

export default function Home() {
  const { query } = useActions();
  const [ taskList, setTaskList ] = useState(null);

  useEffect(() => {
    query(TASK_LIST).then(({ data }) => {
      setTaskList(data.taskList);
    });
  }, [query]);

  if (!taskList) {
    return <div className="loading">Loading...</div>;
  }

  // ·-·-·
}

Here’s an example of how to replace the request call for a mutation operation. In web/src/components/login.js, the changes we have to make are (in bold):

Changes in web/src/components/login.js
import { gql } from "@apollo/client";

const USER_LOGIN = gql`
  mutation userLogin($input: AuthInput!) {
    userLogin(input: $input) {
      errors {
        message
        field
      }
      user {
        id
        name
      }
      authToken
    }
  }
`;

export default function Login({ embedded }) {
  const { mutate, setLocalAppState } = useActions();
  const [ uiErrors, setUIErrors ] = useState();
  const handleLogin = async (event) => {
    event.preventDefault();
    const input = event.target.elements;
    const { data, errors: rootErrors } = await mutate(USER_LOGIN, {
      variables: {
        input: {
          username: input.username.value,
          password: input.password.value,
        },
      },
    });
    // ·-·-·
  };

  // ·-·-·
}

That’s it on the simplest level. You can now test the home page and the login form (the sample data test user’s credentials are "test/123456"). GraphQL operations will be done through Apollo Client instead of the previous axios-based request method.

Go ahead and change all the remaining components to use query/mutate instead of request. Look for request( in your code editor to find all the components that have to be changed. The following git tag has all these changes.

Current code: git checkout v1

(If you need to discard local changes: git reset --hard && git clean -fd)

What we did so far is a basic usage of Apollo Client in a React application, but it’s already paying off. The home page data is now cached after the first hit. If you navigate to a Task page and then back to the home page, Apollo Client will not issue another network request for the taskList.

2.2. Including authentication headers

While caching is great, it introduces some challenges that we need to learn about. To see an example of that, let’s first include the current user’s authToken value in GraphQL requests.

Test the search form right now. Since the new query/mutate are not including the authToken yet, the search operation will not work correctly for private Task entries. As an example, test searching for "babel" after you login with "test/123456".

babel search 1
Figure 6. Searching for "babel" is not working

The logged-in test account owns the sample data Task record about "Babel" but the search field is not currently returning it. We need to include the current authToken value in the headers of GraphQL requests to fix that.

However, now that we’re doing the Ajax requests through Apollo Client, we don’t have direct control over what headers to send. We need to do it "the Apollo Client way". That would be with the "@apollo/link-context" package. That package can be used to change the "context" of the GraphQL operations issued by Apollo Client. To do that, we just create a new "link" object and make it part of Apollo Client’s link "chain".

Start by installing this new package:

npm install @apollo/link-context

Then make the following changes to web/src/store.js to make the new link part of the chain for Apollo Client:

Changes in web/src/store.js
import { setContext } from "@apollo/link-context";

// ·-·-·

export const useStore = () => {
  // ·-·-·

    const authLink = setContext((_, { headers }) => {
    return {
      headers: {
        ...headers,
        authorization: state.user
          ? `Bearer ${state.user.authToken}`
          : "",
      },
    };
  });

  client.setLink(authLink.concat(httpLink));

  // ·-·-·
};

Note how I placed the new authLink within the useStore app so that it can use JavaScript closures to access the state.user object (which is managed within the useStore function).

Now if you search for "babel" while logged-in as "test", it should work.

If the search for "babel" does not work, try restarting your web server to clear any previous cache.

However, there is a problem now. To see it in action, test this flow without refreshing the browser:

  • Login with "test/123456"

  • Search for "babel" (which should work)

  • Logout

  • Search for "babel" again (which should not work).

You’ll notice that the second public search returned the private "Babel" Task entry. WHY?

babel search 2
Figure 7. Cache user session problem

This is happening because of Apollo Client caching. The search for "babel" was cached when the owner was logged in and it remained cached when the owner logged out.

This is a common challenge when dealing with caching. The application logic will often need to manually reset the cache.

Apollo Client provides many methods to work with the cache. You can reset the cache in part or in whole and you can do that either directly after operations (for example, right after the USER_LOGIN mutation) or globally when the state of your application changes. Let’s do the latter. Let’s reset the whole stored cache when the user logs in or out. We can do that in the setLocalAppState context method (which is the one this code uses to make updates to the local app state).

Changes in web/src/store.js
  const setLocalAppState = (newState) => {
    // ·-·-·

    // Reset cache when users login/logout
    if (newState.user || newState.user === null) {
      client.resetStore();
    }
  };

Now, if you test the double search flow again, it should work properly.

You don’t have to reset the whole store. You can reset it in parts. For example, you can clear cached data for a single query using the cache.writeQuery method. We’ll see an example of how to use that later in the lesson.

By the way, you can explore what’s in Apollo Client’s cache using the Apollo Client Devtools extension:

apollo devtools 1
Figure 8. The Cache tab in Apollo Client Devtools

This extension can also be used to monitor active GraphQL operations and manually test them. For example, if you need to manually test the search query in GraphiQL, instead of figuring out how to include the authToken value in headers, the GraphiQL instance that’s integrated in this extension will use the same links chain as your app and include the headers that are already set.

apollo devtools 2
Figure 9. Using the integrated GraphiQL in Apollo Client Devtools

Current code: git checkout v2

(If you need to discard local changes: git reset --hard && git clean -fd)

2.3. Using Apollo hook functions

While we have a working solution to making all GraphQL communications through Apollo Client, this is not the ideal usage of it. We are simply not utilizing a big part of the power offered by it.

Apollo Client offers React hook functions to simplify the logic of React components. The two most common Apollo hook functions are useQuery and useMutation and they are the primary way of using Apollo Client with React. We can use them to replace the query/mutate methods (which are not React-specific).

To be able to use these hook functions in components, we have to wrap the components tree with a "provider" component. The provider component concept is simple: you give it an object and it makes that object available to all the children components in the wrapped tree.

Inspect the code in web/src/components/root.js and see how it uses a Provider component to make the global store object available in children components. Components do not usually use the provided object directly. They use methods which have access to it (through React’s context). The methods that do that in the current store are getLocalAppState, setLocalAppState, and AppLink.

Apollo provider component works in a similar way. You make the client instance itself (which is becoming our new app state store) the provided context value. Children components can then use the hook functions to access and modify Apollo Client’s state (the cache).

React supports having multiple provider wrappers (that provide different contexts). To make the changes minimal for this first step of using Apollo hooks, let’s wrap the components tree as is with the Apollo provider component.

First, let’s remove the query and mutate methods from the store and expose the client instance object itself instead.

Changes in web/src/store.js
export const useStore = () => {
  // ·-·-·

  const authLink = setContext((_, { headers }) => {
    // ·-·-·
  });

  client.setLink(authLink.concat(httpLink));

  // Remove query/mutate methods

  return {
    getLocalAppState,
    setLocalAppState,
    AppLink,
    client,
  };
};
Remember, Apollo Client object still has to be bound to the current local app state store (to access the user state and include current authToken value). Later in this lesson, we’ll see how to manage the app state through Apollo Client itself to simplify the code and use only one global context object.

Apollo exports the ApolloProvider component that can be used to make the client instance available to all children components. Here are the changes in web/src/components/root.js to define and use ApolloProvider:

Changes in web/src/components/root.js
import { ApolloProvider } from "@apollo/client";

import {
  useStore,
  useActions,
  Provider as StoreProvider,
} from "../store";

// ·-·-·

export default function Root({  }) {
  const store = useStore();
  return (
    <ApolloProvider client={store.client}>
      <StoreProvider value={store}>
        <MainRouter />
      </StoreProvider>
    </ApolloProvider>
  );
}
Note how I renamed the previous Provider component StoreProvider because it’s no longer "the" provider. More specific names are better.

With the client instance object available to all components, we can now use Apollo hooks there. Let’s start with the Home component. Here are the changes (in bold) that I made to that component to make it use the useQuery hook function:

Changes in web/src/components/home.js
import React from "react";
import { gql, useQuery } from "@apollo/client";

import TaskSummary, { TASK_SUMMARY_FRAGMENT } from "./task-summary";
import Search from "./search";

const TASK_LIST = gql`
  query taskList {
    taskList {
      id
      ...TaskSummary
    }
  }

  ${TASK_SUMMARY_FRAGMENT}
`;

export default function Home() {
  const { loading, data } = useQuery(TASK_LIST);   (1)

  if (loading) {  (2)
    return <div className="loading">Loading...</div>;
  }

  return (
    <div>
      <Search />
      <div>
        <h1>Latest</h1>
        {data.taskList.map((task) => (
          <TaskSummary key={task.id} task={task} link={true} />
        ))}
      </div>
    </div>
  );
}
1 This invokes the query operation and returns the GraphQL response object and loading state.
2 While the query is pending, the UI can show an indicator. When the query operation is done, React will rerender the component and Apollo sets loading to false.

How simple and nice is that!? Look at the output of git diff on this change to see the logic I was able to replace:

home git diff
Figure 10. Output of git diff for src/components/home.js

The simple useQuery hook function enabled us to replace two React hook functions; useState and useEffect, which were previously used to manually do the data-fetching and manage the request status. This is now all done internally in Apollo Client.

The loading variable is a boolean value that Apollo sets to true while the network data request is pending. useQuery also returns an error variable which holds any GraphQL root errors or network errors (if any). Your UIs should always handle both the loading and error states. For example, we can simply add another if statement and render an error message when the error variable has a value:

Changes in web/src/components/home.js
export default function Home() {
  const { error, loading, data } = useQuery(TASK_LIST);

  if (error) {
    return <div className="error">{error.message}</div>(1)
  }

  if (loading) {
    return <div className="loading">Loading...</div>;
  }

  // ·-·-·
}
1 If useQuery returns an error value, that value is an object which has a message property describing the errors.
When it comes to handling errors in the UI, you should try to make the error branch as close as possible to the data associated with it. For example, the if statement I added above blocks the entire home page including the search box. The search box has nothing to do with any possible errors in the TASK_LIST query. Try to fix that as an exercise.
Apollo Client’s default error policy treats the GraphQL root errors array as network errors (and ignores any partial data). It just throws the error. You can change that behavior by specifying an errorPolicy string value in the operation options object (the second argument of Apollo hook functions). If you specify errorPolicy as "all", Apollo Client will keep the GraphQL root errors array as is for your UI to handle them.

This change in the Home component was a straight-forward one because the replaced code was a common thing components do. We’ll soon see examples of less common things to do with Apollo, but first let’s look at how to do a mutation with the useMutation hook function.

The useMutation hook function is similar to useQuery except it does not send the operation right away. It returns a two-item tuple where the first item is a function that sends the mutation operation when invoked. The second item is the mutation result that is made available right after the function is invoked.

Here’s an example of how we can use both items in the returned tuple:

A useMutation call example
const [ loginUser, { error, loading, data } ] = useMutation(USER_LOGIN);

The loginUser function will do the network request when it’s invoked and it returns the GraphQL response object. For example, we can do the following to invoke loginUser and read the data/errors properties of its GraphQL response object:

Invoking a mutation function
const { data, errors } = await loginUser({
  variables: ·-·-·
});

Here are the changes I made to web/src/components/login.js to make it send its mutation operation with the useMutation hook function:

Changes in web/src/components/login.js
import React, { useState } from "react";
import { gql, useMutation } from "@apollo/client";

// ·-·-·

export default function Login({ embedded }) {
  const { setLocalAppState } = useActions();
  const [ uiErrors, setUIErrors ] = useState();

  const [ loginUser, { error, loading } ] = useMutation(USER_LOGIN); (1)

  if (error) {
    return <div className="error">{error.message}</div>;
  }

  const handleLogin = async (event) => {
    event.preventDefault();
    const input = event.target.elements;
    const { data, errors: rootErrors } = await loginUser({  (2)
      variables: {
        input: {
          username: input.username.value,
          password: input.password.value,
        },
      },
    });
    if (rootErrors) {
      return setUIErrors(rootErrors);
    }
    const { errors, user, authToken } = data.userLogin;
    if (errors.length > 0) {
      return setUIErrors(errors);
    }
    // ·-·-·
  };
  // ·-·-·
}
1 This defines the mutation operation. It does not invoke it.
2 This invokes the mutation operation and returns its GraphQL response object.

Note how the changes were minimal here because the Login component does not have UI state after the mutation is successful (it simply gets unmounted).

What to do with the loading state?

You should change your UIs to indicate that a request is pending. For query operations, this can be as simple as displaying a loading indicator where the data will show up. For mutations, you should at least disable the mutation button to prevent making multiple operations with multiple clicks. I usually also make the button show a loading indicator right there in its label.

For example, in React, here’s how you can make a button disabled and change its label based on a loading boolean variable:

Using the loading state in a button element
<button
  type="submit"
  disabled={loading}
>
  Save {loading && <i className="spinner">...</i>}
</button>

I’ll change all the buttons in this project to reflect the loading state. You can see these changes in the final version of the code.

It’s now time for you to get comfortable with useQuery and useMutation. Start with the TaskPage component. This component currently has three useState calls and one useEffect. Introducing the useQuery method can make us get rid of the useEffect call and one of the three useState calls. Give it a try. Here’s what I ended up doing there:

Changes in web/src/components/task-page.js
import React, { useState } from "react";
import { gql, useQuery } from "@apollo/client";

// ·-·-·

export default function TaskPage({ taskId }) {
  const { AppLink } = useActions();
  // const [ taskInfo, setTaskInfo ] = useState(null);
  const [ showAddApproach, setShowAddApproach ] = useState(false);
  const [ highlightedApproachId, setHighlightedApproachId ] = useState();

  const { error, loading, data } = useQuery(TASK_INFO, {
    variables: { taskId },
  });

  if (error) {
    return <div className="error">{error.message}</div>;
  }

  if (loading) {
    return <div className="loading">Loading...</div>;
  }

  const { taskInfo } = data;

   const handleAppendNewApproach = (newApproach) => {
    // setTaskInfo((pTask) => ({
    //   ...pTask,
    //   approachList: [newApproach, ...pTask.approachList],
    // }));
     setHighlightedApproachId(newApproach.id);
     setShowAddApproach(false);
   };

  return (
    // ·-·-·
  );
}

The remaining useState objects (showAddApproach and highlightedApproachId) manage state elements that are local to this component. Apollo usually is not used for this type of local component state.

I commented out the part that handles appending a new Approach record to the list of Approaches under a Task object. Now that the Task object is managed in the Apollo cache, we’ll have to figure out what to do to append an Approach record to it. We’ll talk about that soon.
There is another popular project, named react-query, which also offers useQuery/useMutation methods. It offers the same concept of fetching and updating asynchronous data in React. The project can be used with any promise-based data request. You can use it with REST APIs, for example.

2.4. Leveraging the automatic cache

Let’s now redo the Approach component and replace the mutate method with useMutation. That component has one mutation to update the voteCount of an Approach object.

This redo will come with a nice little surprise, but before I tell you about it, go ahead and try to do it on your own first.

Here’s what I did. Try to figure out what surprise I am talking about:

.Changes in web/src/components/approach.js
import React from "react";
import { gql, useMutation } from "@apollo/client";

// ·-·-·

export default function Approach({ approach, isHighlighted }) {
  const [ uiErrors, setUIErrors ] = useState([]);
  const [ submitVote, { error, loading } ] = useMutation(APPROACH_VOTE);

  if (error) {
    return <div className="error">{error.message}</div>;
  }

  const handleVote = (direction) => async (event) => {
    event.preventDefault();
    const { data, errors: rootErrors } = await submitVote({
      variables: {
        approachId: approach.id,
        up: direction === "UP",
      },
    });
    if (rootErrors) {
      return setUIErrors(rootErrors);
    }
    const { errors } = data.approachVote;
    if (errors.length > 0) {
      return setUIErrors(errors);
    }
  };

  // ·-·-·

  return (
    <div className={`box highlighted-${isHighlighted}`}>
      <div className="approach">
        <div className="vote">
          {renderVoteButton("UP")}
          {approach.voteCount}
          {renderVoteButton("DOWN")}
        </div>

        {/* ·-·-· */}
    </div>
  );
}

Note how I am now always handling the error/loading states. Apollo just makes that so easy. I previously even skipped that part because it meant adding a new useState call. In addition to making the error/loading/data states easy to use, Apollo also makes the code required to use them similar and often re-usable.

Did you find the little surprise? I was able to get rid of the voteCount local state that was there before to reflect the voting result in the UI. Yet, the voting count still gets updated in the UI with this new code (test it). So, how exactly is it working without the local state?

The answer is once again, the cache!

Instead of the voteCount local state, I made the UI use approach.voteCount directly. I also included the id and voteCount fields in the mutation data (and remember Apollo auto-adds the __typename field as well). When this mutation’s data is received, Apollo uses the unique "Approach:id" identifier to update the identified Approach object. I didn’t even use the data part of this mutation but under the hood, Apollo Client did!

You can test this cache update by removing the id or voteCount fields (or both) from the mutation result. If you do that, the vote count UI will not get updated.

Automatically updated cache is great but oftentimes we need to manually update the cache after a mutation operation. Let’s take a look at that next.

2.5. Manually updating the cache

The Apollo cache is not automatically updated when a mutation modifies multiple objects or when it creates or deletes objects. It only gets updated when the mutation updates a single object.

In the AZdev app, we have a mutation operation that creates a new Approach object (in the NewApproach component). Apollo will not automatically update its cache for this operation.

The code I commented out in the TaskPage component manually appends a newly created Approach object to a local state element it manages for that purpose. See the handleAppendNewApproach function.

Since all the Approach objects under a Task object are now managed in Apollo’s cache, instead of using a state element to append a new Approach object, we’ll have to update Apollo’s cache to append a new Approach object in memory.

We can update the cache in either the NewApproach or TaskPage component. Apollo useMutation hook function accepts an update function (in its options argument) and it invokes that function after a mutation operation is successful. That function receives the cache object and the results object for the mutation operation. For example, here’s how that update function can be used with the APPROACH_CREATE mutation:

Changes in web/src/components/new-approach.js
export default function NewApproach({ taskId, onSuccess }) {
  // ·-·-·

  const [ createApproach, { error, loading } ] = useMutation(
    APPROACH_CREATE,
    {
      update(cache, { data: { approachCreate } }) {
        if (approachCreate.approach) {
          // Read the cache for taskInfo
          // Merge the new approach
          // Update the cache
        }
      },
    },
  );

  // ·-·-·
}

Apollo’s cache object manages data by the query operations that resolved them. It provides methods like readQuery and writeQuery to interact with data managed on a certain query (and variables values if any).

The query operation that has to be updated for this example is the one in the TaskPage component. If we’re to do the cache update in the NewApproach component, we’ll need to export the TASK_INFO query (in TaskPage) and import it in NewApproach.

It’ll be a bit easier to update the cache in the TaskPage component instead. That way we can also skip reading the current cache (for the Task object) because it is already used in that component.

The readQuery and writeQuery functions are also available on the client instance object itself. We can get the client instance object in a component using the useApolloClient function. Here are the changes I made to the TaskPage component (in bold) to update the cache after a new Approach object is created successfully:

Changes in web/src/components/task-page.js
import React, { useState } from "react";
import { gql, useApolloClient, useQuery } from "@apollo/client";

// ·-·-·

export default function TaskPage({ taskId }) {
  const { AppLink } = useActions();
  const [ showAddApproach, setShowAddApproach ] = useState(false);
  const [ highlightedApproachId, setHighlightedApproachId ] = useState();

  const { error, loading, data } = useQuery(TASK_INFO, {
    variables: { taskId },
  });

  const client = useApolloClient();

  // ·-·-·

  const { taskInfo } = data;

  const handleAppendNewApproach = (newApproach) => {
    client.writeQuery({    (1)
      query: TASK_INFO,
      variables: { taskId },
      data: {
        taskInfo: {
          ...taskInfo,  (2)
          approachList: [newApproach, ...taskInfo.approachList], (3)
        },
      },
    });
    setHighlightedApproachId(newApproach.id);
    setShowAddApproach(false);
  };

  // ·-·-·
}
1 Update the cache
2 Use the taskInfo object that’s already defined
3 Merge the newApproach record (and put it on top)

Note how I didn’t need a readQuery call because the taskInfo object is already defined (through the useQuery hook function).

Here are the changes (in bold) that I made to the NewApproach component to make it use the useMutation hook functions:

Changes in web/src/components/new-approach.js
import { gql, useMutation } from "@apollo/client";

// ·-·-·

export default function NewApproach({ taskId, onSuccess }) {
  const { getLocalAppState } = useActions();
  const [ detailRows, setDetailRow ] = useState([0]);
  const [ uiErrors, setUIErrors ] = useState([]);

  const [createApproach, { error, loading }] = useMutation(
    APPROACH_CREATE
  );

  // ·-·-·

  const handleNewApproachSubmit = async (event) => {
    // ·-·-·

    const { data, errors: rootErrors } = await createApproach({
      variables: {
        taskId,
        input: {
          content: input.content.value,
          detailList,
        },
      },
    });
    // ·-·-·
  };

  if (error) {
    return <div className="error">{error.message}</div>;
  }

  // ·-·-·
}
Just like readQuery and writeQuery, Apollo cache has readFragment and writeFragment methods that work similarly but with fragments.

2.6. Performing operations conditionally

Let’s convert the Search component code to use Apollo hooks. There is a new challenge here. The query has to be sent conditionally only when the component has a value in the searchTerm prop. How do we make that work with useQuery?

If we just replace the query function with a useQuery hook and get rid of the useEffect hook, the code would look like this:

Changes in web/src/components/search.js
import React from "react";
import { gql, useQuery } from "@apollo/client";

// ·-·-·

const Search = ({ searchTerm = null }) => {
  const { setLocalAppState, AppLink } = useActions();
  const { error, loading, data } = useQuery(SEARCH_RESULTS, {
    variables: { searchTerm },
  });

  if (error) {
    return <div className="error">{error.message}</div>;
  }

  // ·-·-·

  return (
    <div>
      {/* ·-·-· */}
      {data && data.searchResults && (
        <div>
          <h2>Search Results</h2>
          <div className="y-spaced">
            {data.searchResults.length === 0 && (
              <div className="box box-primary">No results</div>
            )}
            {data.searchResults.map((item, index) => (
              <div key={index} className="box box-primary">
                {/* ·-·-· */}
              </div>
            ))}
          </div>
          <AppLink to="Home">{"<"} Home</AppLink>
        </div>
      )}
    </div>
  );
};

However, that would send the query operation request with a null value for searchTerm every time this component renders. This component renders as part of the home page (to display the search from).

So yeah, that will not work:

search query error
Figure 11. The null searchTerm problem

Unfortunately, we cannot put the useQuery call in an if statement. That’s a React requirement for using hooks (see az.dev/rules-of-hooks).

We can solve this problem in a few different ways:

Using the skip option

Apollo’s useQuery method supports a skip boolean option. A true skip value makes Apollo not send the query operation. That’s exactly what we need:

Skipping a useQuery operation
import React from "react";
import { gql, useQuery } from "@apollo/client";

// ·-·-·

const Search = ({ searchTerm = null }) => {
  const { setLocalAppState, AppLink } = useActions();
  const { error, loading, data } = useQuery(SEARCH_RESULTS, {
    variables: { searchTerm },
    skip: !searchTerm,         (1)
  });

  // ·-·-·
};
1 Only perform the query when there is a searchTerm.

Using a "lazy" query

Apollo Client has a useLazyQuery method that does not perform the query right away but rather give you a function to perform the query (similar to how useMutation works). This means we can keep the useEffect hook function and invoke the lazy query function in there:

Using a "lazy" query
import React from "react";
import { gql, useLazyQuery } from "@apollo/client";

// ·-·-·

const Search = ({ searchTerm = null }) => {
  const { setLocalAppState, AppLink } = useActions();
    const [ performSearch, { error, loading, data } ] = useLazyQuery(
      SEARCH_RESULTS,
      { variables: { searchTerm } }
    );

  useEffect(() => {
    if (searchTerm) {
      performSearch();
    }
  }, [searchTerm, performSearch]);

  if (error) {
    return <div className="error">{error.message}</div>;
  }

  // ·-·-·
};

I like this solution a bit better than the first one. I think it’s more flexible and easier to work with. However, both of these solutions (and the original code) are not ideal. I wanted to show you the powerful features of Apollo but if you find yourself needing to use a lazy query or skip a query, see if the problem can be fixed by re-organizing your components using the single-responsibility principle (or other clean code principles).

Using the single-responsibility principle

The problem of the Search component is that it has two responsibilities. It renders a search form and it renders search results. That violates the single-responsibility principle. A component should do only one thing.

In fact, by simply extracting the search results part into a new conditionally-rendered component, the empty-search problem goes away:

Changes in web/src/components/search.js
function SearchResults({ searchTerm }) {
  const { AppLink } = useActions();
  const { error, loading, data } = useQuery(SEARCH_RESULTS, {
    variables: { searchTerm },
  });

  if (error) {
    return <div className="error">{error.message}</div>;
  }

  if (loading) {
    return <div className="loading">{loading}</div>;
  }

  return (
    <div>
      {data.searchResults && (
        {/* ·-·-· */}
      )}
    </div>
  );
}

export default function Search({ searchTerm = null }) {
  const { setLocalAppState } = useActions();

  const handleSearchSubmit = async (event) => {
    event.preventDefault();
    const term = event.target.search.value;
    setLocalAppState({
      component: { name: "Search", props: { searchTerm: term } },
    });
  };

  return (
    <div>
      <div className="main-container">
        <form method="post" onSubmit={handleSearchSubmit}>
          {/* ·-·-· */}
        </form>
      </div>
      {searchTerm && <SearchResults searchTerm={searchTerm} />}
    </div>
  );
}

Because the new SearchResults component only gets rendered when there is a searchTerm, we can use the useQuery function to fetch its data. To put this solution in other words, we simply lifted the condition to whether or not to render a component instead of whether or not to make a query request.

I kept the split simple in this example but I’d go as far as having three components here: one for the search form, one for the search results, and for the search page (which renders the other two).

As an exercise, convert the rest of components to use Apollo hook functions everywhere and test all your changes. Then compare your changes with git tag v3 in the repo, which has the code after I made all the conversions.

Current code: git checkout v3

(If you need to discard local changes: git reset --hard && git clean -fd)

3. Managing local app state

One of my favorite Apollo Client features is how it can be used to manage the "local app state" of an application. The word "local" here does not mean local to a single component. It’s a label for the state data that is not associated with remote data. We have already been using Apollo to manage app state. The difference about the "local app state" we’re going to implement in this section is that it is not mapped to a GraphQL operation supported by the GraphQL API server, or in other words, it cannot be associated with a "remote query". In Apollo, it can be associated with a "local query".

In the AZdev application, we have two local app state elements, the current user and component information. The entire context object in web/src/store.js is there to manage these two elements. Let’s see how Apollo Client can help us get rid of that context object.

The current local app state in the app is managed with a useState call in web/src/store.js. The new state will be managed "externally" to the React application. When the state is managed externally, React components that need to use that state have to "subscribe" to it (to get notified when the external state is changed). Apollo Client useQuery method is a form of subscribing since it’ll cause a React component function to render when the Apollo cache store has any new data for that query.

Remember the client.writeQuery method we used in the TaskPage component to update the cached version of a Task object? The local app state management in Apollo uses that same method to put local app state elements values in the cache as well. However, writeQuery needs a query and the local app state has no such query. In Apollo, we just make up a "fake" query for it.

You can come up with any GraphQL query (regardless of the server schema) and tell Apollo that you’d like to use that query for the purpose of "client-only" data. You can do that for a whole query or part of an existing query. You just put the @client directive on any field in a query to tell Apollo that it’s a client-only field that does not need to be fetched from the server.

So, let’s do that for the user and component local state elements. Here’s the query I made up with for them. Put this in web/src/store.js:

Changes in web/src/store.js
export const LOCAL_APP_STATE = gql`
  query localAppState {
    component @client {
      name
      props
    }
    user @client {
      name
      authToken
    }
  }
`;
Note how I matched the structure the app uses for these elements in their made-up local query. This will keep the changes in the app to a minimum.

Because all the fields in this query have the @client directive, Apollo will not send this whole query to the server. When we use that query in the app, Apollo will just read it directly from the cache.

I’d like to keep local queries separate from the normal "remote" ones. However, you can mix client-only fields with normal fields if you need to.

Now we can use Apollo to read and update that query. For example, in places where we previously used state.user in the store, we can now read it from the cache:

readQuery replaces the state object
const { user } = cache.readQuery({ query: LOCAL_APP_STATE });

To update the user/component objects, instead of the current setState calls in the store, we can do:

writeQuery replaces the setState calls
cache.writeQuery({
  query: LOCAL_APP_STATE,
  data: { ...currentState, ...newState },
})

Since the local app state will now be managed entirely with the client instance object, that object is now the "store" of the application. We don’t need the useStore function (or its useActions hook). We can just define all functions as top-level exports and import them directly in components. This includes the authLink function, which we previously put inside useStore so that it can access the current user authToken.

Also, since setLocalAppState will now be a top-level export, we can move the AppLink function (which depends on it) to be a top-level export as well. We also need to export the client object itself for the ApolloProvider component to access it.

Here are the changes I made to web/src/store.js to make all that happen:

Changes in web/src/store.js
const authLink = setContext((_, { headers }) => {
  const { user } = cache.readQuery({ query: LOCAL_APP_STATE });  (1)
  return {
    headers: {
      ...headers,
      authorization: user ? `Bearer ${user.authToken}` : "",
    },
  };
});

export const client = new ApolloClient({
  link: authLink.concat(httpLink),
  cache,
});

export const useLocalAppState = (...stateMapper) => {
  const { data } = useQuery(LOCAL_APP_STATE);    (2)
  if (stateMapper.length === 1) {
    return data[stateMapper[0]];
  }
  return stateMapper.map((element) => data[element]);
};

export const setLocalAppState = (newState) => {
  if (newState.component) {
    newState.component.props = newState.component.props ?? {};
  }
  const currentState = cache.readQuery({
    query: LOCAL_APP_STATE,
  });
  const updateState = () => {
    cache.writeQuery({          (3)
      query: LOCAL_APP_STATE,
      data: { ...currentState, ...newState },
    });
  };
  if (newState.user || newState.user === null) {
    client.onResetStore(updateState);      (4)
    client.resetStore();
  } else {
    updateState();
  }
};

export const AppLink = ({ children, to, ...props }) => {
  // ·-·-·
};
1 Read local app state from Apollo cache directly with readQuery
2 Use the hook function useQuery to subscribe to local app state
3 Update local app state in the Apollo cache directly with a writeQuery
4 The resetStore call will remove all local app state data. We need to update the local app state query when that happens.

This is a lot simpler than we had before. We no longer manage a context object or any custom hooks to access it. All app state elements are managed in one place.

You can read the local app state with readQuery and useQuery. Notice how I used the readQuery method in places where I needed to read the data just once. However, in React components, you’ll need to use the useQuery method to read local app state. With useQuery, components will be rerendered when the local app state changes. This is why I made the new state getter function (previously getLocalAppState, now useLocalAppState) use the useQuery hook function. Components that use this new getter will be rerendered when the local app state changes.

In React, functions that make use of hook functions (like useQuery) should have a name that starts with "use". This is actually not required but it’s a great way to quickly understand the type of the function and to make sure (both manually and automatically) that your code does not violate React rules of hooks (see az.dev/rules-of-hooks).

The last change we need in the new Apollo-based store is to initialize the state. That was previously done inside useStore but now we can just use a writeQuery call to do that:

Changes in web/src/store.js
const initialLocalAppState = {
  component: { name: "Home", props: {} },
  user: JSON.parse(window.localStorage.getItem("azdev:user")),
};

cache.writeQuery({
  query: LOCAL_APP_STATE,
  data: initialLocalAppState,
});

Now, we need to change the Root component to get rid of the StateProvider context. We don’t need that anymore:

Changes in web/src/components/root.js
import { client } from "../store";

export default function Root() {
  return (
    <ApolloProvider client={client}>
      <MainRouter /> (1)
    </ApolloProvider>
  );
}
1 No more nested providers!

The web/src/components/root.js file also defines the MainRouter component, which has a useActions call to get the current user/component information. We can get rid of that, import the new useLocalAppState function directly from the store module, and replace the old context-based getLocalAppState function:

Changes in web/src/components/root.js
import React from "react";
import { ApolloProvider } from "@apollo/client";

import { client, useLocalAppState } from "../store";

function MainRouter() {
  const [ component, user] = useLocalAppState("component", "user");

  // ·-·-·
}

For the rest of React components, look for useActions, remove it, and replace it with direct import from web/src/store.js. For example, here’s the "git diff" output for what I did in the NewTask component:

new task git diff
Figure 12. The git diff output for the NewTask component

Make similar changes to all components that have a useAction call. The following git tag is pointing at the application code after I made all these changes:

Current code: git checkout v4

(If you need to discard local changes: git reset --hard && git clean -fd)

I hope this little example demonstrated how powerful Apollo local app state management is but we barely scratched the surface. There is a lot more.

For more complex local state elements, you can write custom "resolvers" for your local app state elements. You can also define local mutations and use them with the useMutation methods instead of doing direct writes. Another thing you get with custom resolvers is data types validation (because you define the types for arguments and input).

4. Using GraphQL subscriptions

I saved the best for last! Let’s take a look at how to use GraphQL subscriptions with Apollo Client.

Subscriptions are extremely useful when you need your UIs to auto-update. For example, while looking at the main page list of Tasks, it would be cool if the UI notified the user that there are "new Tasks available". Just like how Twitter notifies you when there are new Tweets on your timeline.

4.1. Polling and refetching

To implement a feature like that, you have two options:

  1. Make your app continuously ask the server about the list of Tasks.

  2. Make your app tell the server that it is interested in new Tasks and that it would like to be notified when they get created.

The second option is what GraphQL subscriptions can help you do. The first option is known as "continuous polling" and sometimes that might be good enough for what you’re trying to do. If the object you’re auto-updating is small and you don’t need "real-time" updates, polling is an option to consider.

Apollo makes continuous polling easy. You actually just add one option to the useQuery second argument to make it repeatedly poll data. For example, we can update the list of Task records in the home page every 5 seconds using this simple change:

An example for pollInterval
export default function Home() {
  const { error, loading, data } = useQuery(TASK_LIST, {
    pollInterval: 5000,
  });

  // ·-·-·
}

That’s it! Now the list will be auto-updated every 5 seconds. Test it by opening 2 browsers and create a Task record in one while looking at the home page in the other.

However, this is a very inefficient way to update this list. We’re refetching the entire list to just get the new records.

In some cases, you can do the refetching manually instead of automatically (in a polling loop). If you want Apollo to always fetch the query when the component rerenders, you can change the fetchPolicy option like this:

An example for fetchPolicy
export default function Home() {
  const { error, loading, data } = useQuery(TASK_LIST, {
    fetchPolicy: "network-only",
  });

  // ·-·-·
}

This would make Apollo ignore the cache and always fetch the query from the server.

If you want Apollo to fetch the query again on demand (for example when user clicks a "refresh" button), you can use the refetch function which Apollo makes available to all useQuery results. For example:

Refetching a query on demand
export default function Home() {
  const { error, loading, refetch, data } = useQuery(TASK_LIST);

  // ·-·-·

  return (
    <div>
      <Search />
      <div>
        <h1>Latest</h1>
        <button onClick={() => refetch()}>Refresh</button>
        {/* ·-·-· */}
      </div>
    </div>
  );
}

This would make Apollo fetch the same query again when the user clicks the refresh button.

However, both of these options fetch the entire list of Task records. GraphQL subscriptions are the much more efficient option to getting new data from an API server. Undo all the polling/refetching changes and let’s auto-update this list with a subscription operation.

4.2. The useSubscription method

To use subscriptions operations, we need to install the "@apollo/link-ws" package:

npm install @apollo/link-ws

This package makes Apollo Client able to do WebSocket communication in the browser. It’s designed to work with GraphQL subscriptions. To initialize it, you just give it the GraphQL subscription URL and an object of options:

Changes in web/src/store.js
import { WebSocketLink } from "@apollo/link-ws";

import {
  GRAPHQL_SERVER_URL,
  GRAPHQL_SUBSCRIPTIONS_URL
} from "./config";

// ·-·-·

const wsLink = new WebSocketLink({
  uri: GRAPHQL_SUBSCRIPTIONS_URL,   (1)
  options: { reconnect: true },     (2)
});
1 What URL to use for subscriptions
2 The reconnect option makes it reconnect in case of connection error
The GRAPHQL_SUBSCRIPTIONS_URL config value is already defined as ws://localhost:4321/graphql, which is the default subscriptions URL created by the API server.

So now we have two main "links" for Apollo Client to use, one with regular HTTP requests (httpLink) and another for WebSocket requests (wsLink). However, instead of making two different client objects for them, Apollo supports a split function that can determine which link object to use based on what GraphQL operation is being invoked:

Changes in web/src/store.js
import {
  ApolloClient,
  HttpLink,
  InMemoryCache,
  gql,
  useQuery,
  split,
} from "@apollo/client";
import { getMainDefinition } from '@apollo/client/utilities';

// ·-·-·

const splitLink = split(
  ({ query }) => {  (1)
    const definition = getMainDefinition(query);   (2)
    return (
      definition.kind === "OperationDefinition" &&   (3)
      definition.operation === "subscription"
    );
  },
  wsLink, (4)
  authLink.concat(httpLink), (5)
);

export const client = new ApolloClient({
  link: splitLink,  (6)
  cache,
});
1 The first argument for split is a function that receives the operation to be invoked. It should return either true or false.
2 getMainDefinition returns the AST of the first main operation (query, mutation, or subscription)
3 If the main operation is a subscription, this condition will be true
4 If the first argument to split returns true, the link in the second argument will be used
5 If the first argument to split returns false, the link in the third argument will be used. This is the link that’s currently used for all regular HTTP requests.

Apollo Client will invoke this new split function for each GraphQL operation it needs to send over the wire. If the operation is a subscription, the split function tells Apollo Client to use wsLink. Otherwise it tells it to use httpLink. This enables us to work with only one client instance everywhere in the app.

That’s all the setup work we need to make Apollo Client ready for subscriptions. To make a React component use a subscription operation, we just invoke the useSubscription hook function. For example, here’s all the code we now need to make the vote counts on Approach objects update in real-time:

Changes in web/src/components/task-page.js
import {
  gql,
  useApolloClient,
  useQuery,
  useSubscription,
} from "@apollo/client";

// ·-·-·

const VOTE_CHANGED = gql`
  subscription voteChanged($taskId: ID!) {
    voteChanged(taskId: $taskId) {
      id
      voteCount
    }
  }
`;

export default function TaskPage({ taskId }) {
  // ·-·-·

  const { error, loading, data } = useQuery(TASK_INFO, {
    variables: { taskId },
  });

  useSubscription(VOTE_CHANGED, {
    variables: { taskId }
  });

  const client = useApolloClient();

  // ·-·-·
}

That’s it. You can test that with two browsers opened on the same Task page and vote up/down on any Approach in one browser. The other browser will update in real-time!

Under the hood, Apollo takes care of figuring out that this subscription is bringing updates related to the objects displayed on this page. The useQuery results get auto-refreshed causing the TaskPage component to rerender with the new votes.

Try to use the taskListChanged subscription on your own. The changes for this one are in the Home component (web/src/components/home.js). To keep things simple, when a new Task is fetched through the subscription, insert it on top of the list and highlight it differently. Here are some hints to help you with this challenge:

  • The taskListChanged subscription does not need variables

  • Unlike voteChanged, you’ll need to use the data of this subscription. You can access a useSubscription operation data the same way you do with useQuery.

  • When subscription data is made available, you’ll need to make the UI render a new element of the TaskSummary component.

I’ve put my solution to that in the final git tag.

Current code: git checkout v5

(If you need to discard local changes: git reset --hard && git clean -fd)


I hope this lesson was helpful! Checkout my GraphQL In Action book where the full more featured AZdev project is explained. Thanks for reading!