Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.
at main 6.4 kB view raw
1import { useState, useCallback, useRef, useEffect } from 'preact/hooks'; 2import { pipe, onPush, filter, toPromise, take } from 'wonka'; 3 4import type { 5 AnyVariables, 6 DocumentInput, 7 OperationResult, 8 OperationContext, 9 CombinedError, 10 Operation, 11} from '@urql/core'; 12import { createRequest } from '@urql/core'; 13 14import { useClient } from '../context'; 15import { initialState } from './constants'; 16 17/** State of the last mutation executed by your {@link useMutation} hook. 18 * 19 * @remarks 20 * `UseMutationState` is returned (in a tuple) by {@link useMutation} and 21 * gives you the {@link OperationResult} of the last mutation executed 22 * with {@link UseMutationExecute}. 23 * 24 * Even if the mutation document passed to {@link useMutation} changes, 25 * the state isn’t reset, so you can keep displaying the previous result. 26 */ 27export interface UseMutationState< 28 Data = any, 29 Variables extends AnyVariables = AnyVariables, 30> { 31 /** Indicates whether `useMutation` is currently executing a mutation. */ 32 fetching: boolean; 33 /** Indicates that the mutation result is not fresh. 34 * 35 * @remarks 36 * The `stale` flag is set to `true` when a new result for the mutation 37 * is expected. 38 * This is mostly unused for mutations and will rarely affect you, and 39 * is more relevant for queries. 40 * 41 * @see {@link OperationResult.stale} for the source of this value. 42 */ 43 stale: boolean; 44 /** The {@link OperationResult.data} for the executed mutation. */ 45 data?: Data; 46 /** The {@link OperationResult.error} for the executed mutation. */ 47 error?: CombinedError; 48 /** The {@link OperationResult.hasNext} for the executed query. */ 49 hasNext: boolean; 50 /** The {@link OperationResult.extensions} for the executed mutation. */ 51 extensions?: Record<string, any>; 52 /** The {@link Operation} that the current state is for. 53 * 54 * @remarks 55 * This is the mutation {@link Operation} that has last been executed. 56 * When {@link UseQueryState.fetching} is `true`, this is the 57 * last `Operation` that the current state was for. 58 */ 59 operation?: Operation<Data, Variables>; 60} 61 62/** Triggers {@link useMutation} to execute its GraphQL mutation operation. 63 * 64 * @param variables - variables using which the mutation will be executed. 65 * @param context - optionally, context options that will be merged with the hook's 66 * {@link UseQueryArgs.context} options and the `Client`’s options. 67 * @returns the {@link OperationResult} of the mutation. 68 * 69 * @remarks 70 * When called, {@link useMutation} will start the GraphQL mutation 71 * it currently holds and use the `variables` passed to it. 72 * 73 * Once the mutation response comes back from the API, its 74 * returned promise will resolve to the mutation’s {@link OperationResult} 75 * and the {@link UseMutationState} will be updated with the result. 76 * 77 * @example 78 * ```ts 79 * const [result, executeMutation] = useMutation(UpdateTodo); 80 * const start = async ({ id, title }) => { 81 * const result = await executeMutation({ id, title }); 82 * }; 83 */ 84export type UseMutationExecute< 85 Data = any, 86 Variables extends AnyVariables = AnyVariables, 87> = ( 88 variables: Variables, 89 context?: Partial<OperationContext> 90) => Promise<OperationResult<Data, Variables>>; 91 92/** Result tuple returned by the {@link useMutation} hook. 93 * 94 * @remarks 95 * Similarly to a `useState` hook’s return value, 96 * the first element is the {@link useMutation}’s state, updated 97 * as mutations are executed with the second value, which is 98 * used to start mutations and is a {@link UseMutationExecute} 99 * function. 100 */ 101export type UseMutationResponse< 102 Data = any, 103 Variables extends AnyVariables = AnyVariables, 104> = [UseMutationState<Data, Variables>, UseMutationExecute<Data, Variables>]; 105 106/** Hook to create a GraphQL mutation, run by passing variables to the returned execute function. 107 * 108 * @param query - a GraphQL mutation document which `useMutation` will execute. 109 * @returns a {@link UseMutationResponse} tuple of a {@link UseMutationState} result, 110 * and an execute function to start the mutation. 111 * 112 * @remarks 113 * `useMutation` allows GraphQL mutations to be defined and keeps its state 114 * after the mutation is started with the returned execute function. 115 * 116 * Given a GraphQL mutation document it returns state to keep track of the 117 * mutation state and a {@link UseMutationExecute} function, which accepts 118 * variables for the mutation to be executed. 119 * Once called, the mutation executes and the state will be updated with 120 * the mutation’s result. 121 * 122 * @see {@link https://urql.dev/goto/docs/basics/react-preact/#mutations} for `useMutation` docs. 123 * 124 * @example 125 * ```ts 126 * import { gql, useMutation } from '@urql/preact'; 127 * 128 * const UpdateTodo = gql` 129 * mutation ($id: ID!, $title: String!) { 130 * updateTodo(id: $id, title: $title) { 131 * id, title 132 * } 133 * } 134 * `; 135 * 136 * const UpdateTodo = () => { 137 * const [result, executeMutation] = useMutation(UpdateTodo); 138 * const start = async ({ id, title }) => { 139 * const result = await executeMutation({ id, title }); 140 * }; 141 * // ... 142 * }; 143 * ``` 144 */ 145export function useMutation< 146 Data = any, 147 Variables extends AnyVariables = AnyVariables, 148>(query: DocumentInput<Data, Variables>): UseMutationResponse<Data, Variables> { 149 const isMounted = useRef(true); 150 const client = useClient(); 151 152 const [state, setState] = 153 useState<UseMutationState<Data, Variables>>(initialState); 154 155 const executeMutation = useCallback( 156 (variables: Variables, context?: Partial<OperationContext>) => { 157 setState({ ...initialState, fetching: true }); 158 return pipe( 159 client.executeMutation<Data, Variables>( 160 createRequest<Data, Variables>(query, variables), 161 context || {} 162 ), 163 onPush(result => { 164 if (isMounted.current) { 165 setState({ 166 fetching: false, 167 stale: result.stale, 168 data: result.data, 169 hasNext: result.hasNext, 170 error: result.error, 171 extensions: result.extensions, 172 operation: result.operation, 173 }); 174 } 175 }), 176 filter(result => !result.hasNext), 177 take(1), 178 toPromise 179 ); 180 }, 181 // eslint-disable-next-line react-hooks/exhaustive-deps 182 [client, query, setState] 183 ); 184 185 useEffect(() => { 186 return () => { 187 isMounted.current = false; 188 }; 189 }, []); 190 191 return [state, executeMutation]; 192}