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