UNPKG

@apollo/client

Version:

A fully-featured caching GraphQL client.

550 lines (459 loc) 12.1 kB
# Mutations Reference ## Table of Contents - [useMutation Hook](#usemutation-hook) - [Mutation Variables](#mutation-variables) - [Loading and Error States](#loading-and-error-states) - [Optimistic UI](#optimistic-ui) - [Cache Updates](#cache-updates) - [Refetch Queries](#refetch-queries) - [Error Handling](#error-handling) ## useMutation Hook The `useMutation` hook is used to execute GraphQL mutations. ### Basic Usage ```tsx import { gql } from "@apollo/client"; import { useMutation } from "@apollo/client/react"; const ADD_TODO = gql` mutation AddTodo($text: String!) { addTodo(text: $text) { id text completed } } `; function AddTodo() { const [addTodo, { data, loading, error }] = useMutation(ADD_TODO); return ( <form onSubmit={(e) => { e.preventDefault(); const form = e.currentTarget; const text = new FormData(form).get("text") as string; addTodo({ variables: { text } }); form.reset(); }} > <input name="text" placeholder="Add todo" /> <button type="submit" disabled={loading}> Add </button> {error && <p>Error: {error.message}</p>} </form> ); } ``` ### Return Tuple ```typescript const [ mutateFunction, // Function to call to execute mutation { data, // Mutation result data loading, // True while mutation is in flight error, // ApolloError if mutation failed called, // True if mutation has been called reset, // Reset mutation state client, // Apollo Client instance }, ] = useMutation(MUTATION); ``` ## Mutation Variables ### Variables in Options ```tsx const [createUser] = useMutation(CREATE_USER, { variables: { input: { name: "Default User", email: "default@example.com", }, }, }); // Call with default variables await createUser(); // Override variables await createUser({ variables: { input: { name: "Custom User", email: "custom@example.com", }, }, }); ``` ### TypeScript Types Use `TypedDocumentNode` instead of generic type parameters: ```typescript import { gql, TypedDocumentNode } from "@apollo/client"; interface CreateUserData { createUser: { id: string; name: string; email: string; }; } interface CreateUserVariables { input: { name: string; email: string; }; } const CREATE_USER: TypedDocumentNode<CreateUserData, CreateUserVariables> = gql` mutation CreateUser($input: CreateUserInput!) { createUser(input: $input) { id name email } } `; const [createUser, { data, loading }] = useMutation(CREATE_USER); const { data } = await createUser({ variables: { input: { name: "John", email: "john@example.com" }, }, }); // data.createUser is fully typed ``` ## Loading and Error States ### Handling in UI ```tsx function CreatePost() { const [createPost, { loading, error, data, reset }] = useMutation(CREATE_POST); if (data) { return ( <div> <p>Post created: {data.createPost.title}</p> <button onClick={reset}>Create another</button> </div> ); } return ( <form onSubmit={handleSubmit}> <input name="title" disabled={loading} /> <textarea name="content" disabled={loading} /> <button type="submit" disabled={loading}> {loading ? "Creating..." : "Create Post"} </button> {error && ( <div className="error"> <p>Failed to create post: {error.message}</p> <button onClick={reset}>Try again</button> </div> )} </form> ); } ``` ### Async/Await Pattern If you only need the promise without using the hook's loading/data state, use `client.mutate` instead: ```tsx import { useApolloClient } from "@apollo/client/react"; function CreatePost() { const client = useApolloClient(); async function handleSubmit(formData: FormData) { try { const { data } = await client.mutate({ mutation: CREATE_POST, variables: { input: { title: formData.get("title"), content: formData.get("content"), }, }, }); console.log("Created:", data.createPost); router.push(`/posts/${data.createPost.id}`); } catch (error) { console.error("Failed to create post:", error); } } return ( <form onSubmit={(e) => { e.preventDefault(); handleSubmit(new FormData(e.currentTarget)); }} > ... </form> ); } ``` If you do use the hook's state, e.g. because you want to render the `loading` state, errors or returned `data`, you can also use the `useMutation` hook with `async..await` in your handler: ```tsx function CreatePost() { const [createPost, { loading }] = useMutation(CREATE_POST); async function handleSubmit(formData: FormData) { try { const { data } = await createPost({ variables: { input: { title: formData.get("title"), content: formData.get("content"), }, }, }); console.log("Created:", data.createPost); router.push(`/posts/${data.createPost.id}`); } catch (error) { console.error("Failed to create post:", error); } } return ( <form onSubmit={(e) => { e.preventDefault(); handleSubmit(new FormData(e.currentTarget)); }} > <button type="submit" disabled={loading}> {loading ? "Creating..." : "Create Post"} </button> </form> ); } ``` ## Optimistic UI Optimistic UI immediately reflects the expected result of a mutation before the server responds. ### Basic Optimistic Response **Important**: `optimisticResponse` needs to be a full valid response for the mutation. A partial result might result in subtle errors. ```tsx const [addTodo] = useMutation(ADD_TODO, { optimisticResponse: { addTodo: { __typename: "Todo", id: "temp-id", text: "New todo", completed: false, }, }, }); ``` ### Dynamic Optimistic Response ```tsx function TodoList() { const [addTodo] = useMutation(ADD_TODO); const handleAdd = (text: string) => { addTodo({ variables: { text }, optimisticResponse: { addTodo: { __typename: "Todo", id: `temp-${Date.now()}`, text, completed: false, }, }, }); }; return <AddTodoForm onAdd={handleAdd} />; } ``` ### Optimistic Response with Cache Update ```tsx const [toggleTodo] = useMutation(TOGGLE_TODO, { optimisticResponse: ({ id }) => ({ toggleTodo: { __typename: "Todo", id, completed: true, // Assume success }, }), update: (cache, { data }) => { // This runs twice: once with optimistic data, once with server data cache.modify({ id: cache.identify(data.toggleTodo), fields: { completed: () => data.toggleTodo.completed, }, }); }, }); ``` ## Cache Updates ### Using update Function ```tsx const [addTodo] = useMutation(ADD_TODO, { update: (cache, { data }) => { // Read existing todos from cache const existingTodos = cache.readQuery<{ todos: Todo[] }>({ query: GET_TODOS, }); // Write updated list back to cache cache.writeQuery({ query: GET_TODOS, data: { todos: [...(existingTodos?.todos ?? []), data.addTodo], }, }); }, }); ``` ### cache.modify ```tsx const [deleteTodo] = useMutation(DELETE_TODO, { update: (cache, { data }) => { cache.modify({ fields: { todos: (existingTodos: Reference[], { readField }) => { return existingTodos.filter( (todoRef) => readField("id", todoRef) !== data.deleteTodo.id ); }, }, }); }, }); ``` ### cache.evict ```tsx const [deleteUser] = useMutation(DELETE_USER, { update: (cache, { data }) => { // Remove the user object from cache entirely cache.evict({ id: cache.identify(data.deleteUser) }); // Clean up dangling references cache.gc(); }, }); ``` ### Updating Related Queries ```tsx const [createPost] = useMutation(CREATE_POST, { update: (cache, { data }) => { // Update author's post count cache.modify({ id: cache.identify({ __typename: "User", id: data.createPost.authorId }), fields: { postCount: (existing) => existing + 1, posts: (existing, { toReference }) => [ ...existing, toReference(data.createPost), ], }, }); // Add to feed cache.modify({ fields: { feed: (existing, { toReference }) => [ toReference(data.createPost), ...existing, ], }, }); }, }); ``` ## Refetch Queries ### Basic Refetch There are three refetch notations: - **String**: `refetchQueries: ['getTodos']` - refetches all active `getTodos` queries - **Query document**: `refetchQueries: [GET_TODOS]` - refetches all active queries using this document - **Object**: `refetchQueries: [{ query: GET_TODOS }, { query: GET_TODOS, variables: { page: 25 } }]` - **fetches** the query, regardless if it's actively used in the UI ```tsx const [addTodo] = useMutation(ADD_TODO, { // Refetch all active GET_TODOS queries refetchQueries: ["getTodos"], // Or: refetchQueries: [GET_TODOS], }); // Fetch specific query with variables (even if not active) const [addTodo] = useMutation(ADD_TODO, { refetchQueries: [{ query: GET_TODOS }, { query: GET_TODO_COUNT }], }); ``` ### Conditional Refetch ```tsx const [addTodo] = useMutation(ADD_TODO, { refetchQueries: (result) => { if (result.data?.addTodo.priority === "HIGH") { return [{ query: GET_HIGH_PRIORITY_TODOS }]; } return [{ query: GET_TODOS }]; }, }); ``` ### Refetch Active Queries ```tsx const [addTodo] = useMutation(ADD_TODO, { refetchQueries: "active", // Refetch all active queries // Or: 'all' to refetch all queries (including inactive) }); ``` ### awaitRefetchQueries ```tsx const [addTodo] = useMutation(ADD_TODO, { refetchQueries: [{ query: GET_TODOS }], awaitRefetchQueries: true, // Wait for refetch before resolving mutation }); ``` ### onQueryUpdated Returning `true` from `onQueryUpdated` causes a refetch. Don't call `refetch()` manually inside `onQueryUpdated`, as it won't retain the query and might cancel it early. ```tsx const [addTodo] = useMutation(ADD_TODO, { update: (cache, { data }) => { // Update cache... }, onQueryUpdated: (observableQuery) => { // Called for each query affected by cache update console.log(`Query ${observableQuery.queryName} was updated`); // Return true to refetch return true; }, }); ``` ## Error Handling ### Error Policy ```tsx const [createUser, { loading }] = useMutation(CREATE_USER, { errorPolicy: "all", // Return both data and errors }); const { data, errors } = await createUser({ variables: { input }, }); // Handle partial success if (data?.createUser) { console.log("User created:", data.createUser); } if (errors) { console.warn("Some errors occurred:", errors); } ``` ### onError Callback ```tsx const [createUser] = useMutation(CREATE_USER, { onError: (error) => { // Handle error globally toast.error(`Failed to create user: ${error.message}`); // Log to error tracking service Sentry.captureException(error); }, onCompleted: (data) => { toast.success(`User ${data.createUser.name} created!`); }, }); ``` ### Field-Level Errors ```tsx const [createUser] = useMutation(CREATE_USER, { errorPolicy: "all", }); const handleSubmit = async (input: CreateUserInput) => { const { data, errors } = await createUser({ variables: { input }, }); // Handle GraphQL validation errors const fieldErrors = errors?.reduce( (acc, error) => { const field = error.extensions?.field as string; if (field) { acc[field] = error.message; } return acc; }, {} as Record<string, string> ); if (fieldErrors?.email) { setEmailError(fieldErrors.email); } }; ```