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 mutation accepts a single variable, 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:
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) {idtoken}}`;
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 fields 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 GraphQL operations
Apply the useMutation
hook
We'll use Apollo Client's useMutation
React Hook to execute our LOGIN_USER
mutation. As with useQuery
, the hook's result provides properties that help us populate and render our component throughout the mutation's execution.
Unlike useQuery
, useMutation
doesn't execute its operation as soon as its component renders. Instead, the hook returns a mutate function that we call to execute the mutation whenever we want (such as when the user submits a form).
Add the following to the bottom of 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 mutation. We pass this function to ourLoginForm
component. - The second object in the tuple is similar to the result object returned by
useQuery
, including fields for the operation'sloading
anderror
states and the operation's resultdata
.
Now whenever a user submits the login form, our login
mutation 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 mutation'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:
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.
Add Authorization
headers to all requests
Our client should provide the user's token with each GraphQL operation 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 GraphQL request:
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 operations that don't require it (such as fetching the list of launches), so it's fine for our client to include the token in every request.
Enable the login form
We're finished defining our login
mutation, 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 Apollo Client's local state APIs to power some of the form's logic in the next section.