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}