Update data with mutations
Learn how to update data with the useMutation hook
Time to accomplish: 10 minutes
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.
As a reminder, mutations are GraphQL operations that can modify back-end data (unlike queries). Our server's schema supports the following mutations:
# YOU DON'T NEED TO COPY THIS.
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.
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
.jsand.jsxfile extensions wherever.tsand.tsxappear.
Support user login
Note: For simplicity, our example application doesn't implement actual user accounts with password-based authentication. Instead, a user unsecurely "logs in" by submitting their email address and receiving a corresponding session token from the server.
Define the mutation
To start, navigate to 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) {
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 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 ourLoginFormcomponent. - The second object in the tuple is similar to the result object returned by
useQuery, including fields for the operation'sloadinganderrorstates 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 chapter.
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.