9. Updating data with mutations
10m

Updating data with the useMutation hook

Now that we've added multiple queries to our React app, let's add a mutation. The process is similar, with a few important differences.

type Mutation {
bookTrips(launchIds: [ID]!): TripUpdateResponse!
cancelTrip(launchId: ID!): TripUpdateResponse!
login(email: String): String # login token
}

We'll start by implementing the ability to log in. Notice that this accepts a single , email.

Support user login

Note: For simplicity, our example application doesn't implement actual user accounts with password-based authentication. Instead, a user un-securely "logs in" by submitting their email address and receiving a corresponding session token from the server.

Define the mutation

The code blocks below use TypeScript by default. You can use the dropdown menu above each code block to switch to JavaScript.


If you're using JavaScript, use `.js` and `.jsx` file extensions wherever `.ts` and `.tsx` appear.

To start, navigate to client/src/pages/login.tsx and replace its contents with the following:

client/src/pages/login.tsx
import React from "react";
import { gql, useMutation } from "@apollo/client";
import { LoginForm, Loading } from "../components";
import * as LoginTypes from "./__generated__/Login";
export const LOGIN_USER = gql`
mutation Login($email: String!) {
login(email: $email) {
id
token
}
}
`;

Our LOGIN_USER definition looks just like our queries from the previous section, except it replaces the word query with mutation. We receive a User object in the response from login, which includes two that we'll use:

  • The user's id, which we'll use to fetch user-specific data in future queries
  • A session token, which we'll use to "authenticate" future s

Apply the useMutation hook

We'll use 's useMutation React Hook to execute our LOGIN_USER . As with useQuery, the hook's result provides properties that help us populate and render our component throughout the 's execution.

Unlike useQuery, useMutation doesn't execute its as soon as its component renders. Instead, the hook returns a mutate function that we call to execute the whenever we want (such as when the user submits a form).

Add the following to the bottom of login.tsx:

client/src/pages/login.tsx
export default function Login() {
const [login, { loading, error }] = useMutation<
LoginTypes.Login,
LoginTypes.LoginVariables
>(LOGIN_USER);
if (loading) return <Loading />;
if (error) return <p>An error occurred</p>;
return <LoginForm login={login} />;
}
  • The first object in useMutation's result tuple (login) is the mutate function we call to execute the . We pass this function to our LoginForm component.
  • The second object in the tuple is similar to the result object returned by useQuery, including for the 's loading and error states and the 's result data.

Now whenever a user submits the login form, our login is called. The user's token is stored in the in-memory cache, however we want that token to be available across multiple visits in the same browser. Let's tackle that next.

Persist the user's token and ID

In our call to useMutation, we can include an onCompleted callback. This enables us to interact with the 's result data as soon as it's available. We'll use this callback to persist the user's token and id.

Modify the useMutation call in login.tsx to match the following:

client/src/pages/login.tsx
const [login, { loading, error }] = useMutation<
LoginTypes.Login,
LoginTypes.LoginVariables
>(LOGIN_USER, {
onCompleted({ login }) {
if (login) {
localStorage.setItem("token", login.token as string);
localStorage.setItem("userId", login.id as string);
}
},
});

Our onCompleted callback stores the user's unique ID and session token in localStorage, so we can load these values into the in-memory cache the next time the user visits our application. We'll add that functionality in the next lesson.

Task!

Add Authorization headers to all requests

Our client should provide the user's token with each it sends to our server. This enables the server to verify that the user has permission to do what they're trying to do.

In index.tsx, let's modify the constructor of ApolloClient to define a default set of headers that are applied to every request:

client/src/index.tsx
const client: ApolloClient<NormalizedCacheObject> = new ApolloClient({
cache,
uri: "http://localhost:4000/graphql",
headers: {
authorization: localStorage.getItem("token") || "",
},
});

Our server can ignore the token when resolving that don't require it (such as fetching the list of ), so it's fine for our client to include the token in every request.

Task!

Enable the login form

We're finished defining our login , but we don't yet display the form that enables a user to execute it. Because we're storing the user token locally, we'll use 's local state APIs to power some of the form's logic in the next section.

Previous