1/* eslint-disable react-hooks/rules-of-hooks */
2
3import type { Ref, WatchStopHandle } from 'vue';
4import { shallowRef, watchEffect } from 'vue';
5
6import type { Subscription } from 'wonka';
7import { pipe, subscribe, onEnd } from 'wonka';
8
9import type {
10 Client,
11 AnyVariables,
12 GraphQLRequestParams,
13 CombinedError,
14 OperationContext,
15 RequestPolicy,
16 Operation,
17} from '@urql/core';
18
19import { useClient } from './useClient';
20
21import type { MaybeRefOrGetter, MaybeRefOrGetterObj } from './utils';
22import { useRequestState, useClientState } from './utils';
23
24/** Input arguments for the {@link useQuery} function.
25 *
26 * @param query - The GraphQL query that `useQuery` executes.
27 * @param variables - The variables for the GraphQL query that `useQuery` executes.
28 */
29export type UseQueryArgs<
30 Data = any,
31 Variables extends AnyVariables = AnyVariables,
32> = {
33 /** Updates the {@link RequestPolicy} for the executed GraphQL query operation.
34 *
35 * @remarks
36 * `requestPolicy` modifies the {@link RequestPolicy} of the GraphQL query operation
37 * that `useQuery` executes, and indicates a caching strategy for cache exchanges.
38 *
39 * For example, when set to `'cache-and-network'`, {@link useQuery} will
40 * receive a cached result with `stale: true` and an API request will be
41 * sent in the background.
42 *
43 * @see {@link OperationContext.requestPolicy} for where this value is set.
44 */
45 requestPolicy?: MaybeRefOrGetter<RequestPolicy>;
46 /** Updates the {@link OperationContext} for the executed GraphQL query operation.
47 *
48 * @remarks
49 * `context` may be passed to {@link useQuery}, to update the {@link OperationContext}
50 * of a query operation. This may be used to update the `context` that exchanges
51 * will receive for a single hook.
52 *
53 * @example
54 * ```ts
55 * const result = useQuery({
56 * query,
57 * context: {
58 * additionalTypenames: ['Item'],
59 * },
60 * });
61 * ```
62 */
63 context?: MaybeRefOrGetter<Partial<OperationContext>>;
64 /** Prevents {@link useQuery} from automatically executing GraphQL query operations.
65 *
66 * @remarks
67 * `pause` may be set to `true` to stop {@link useQuery} from executing
68 * automatically. This will pause the query until {@link UseQueryState.resume}
69 * is called, or, if `pause` is a reactive ref of a boolean, until this
70 * ref changes to `true`.
71 *
72 * @see {@link https://urql.dev/goto/docs/basics/vue#pausing-usequery} for
73 * documentation on the `pause` option.
74 */
75 pause?: MaybeRefOrGetter<boolean>;
76} & MaybeRefOrGetterObj<GraphQLRequestParams<Data, Variables>>;
77
78/** State of the current query, your {@link useQuery} function is executing.
79 *
80 * @remarks
81 * `UseQueryState` is returned by {@link useQuery} and
82 * gives you the updating {@link OperationResult} of
83 * GraphQL queries.
84 *
85 * Each value that is part of the result is wrapped in a reactive ref
86 * and updates as results come in.
87 *
88 * Hint: Even when the query and variables update, the previous state of
89 * the last result is preserved, which allows you to display the
90 * previous state, while implementing a loading indicator separately.
91 */
92export interface UseQueryState<T = any, V extends AnyVariables = AnyVariables> {
93 /** Indicates whether `useQuery` is waiting for a new result.
94 *
95 * @remarks
96 * When `useQuery` receives a new query and/or variables, it will
97 * start executing the new query operation and `fetching` is set to
98 * `true` until a result arrives.
99 *
100 * Hint: This is subtly different than whether the query is actually
101 * fetching, and doesn’t indicate whether a query is being re-executed
102 * in the background. For this, see {@link UseQueryState.stale}.
103 */
104 fetching: Ref<boolean>;
105 /** Indicates that the state is not fresh and a new result will follow.
106 *
107 * @remarks
108 * The `stale` flag is set to `true` when a new result for the query
109 * is expected and `useQuery` is waiting for it. This may indicate that
110 * a new request is being requested in the background.
111 *
112 * @see {@link OperationResult.stale} for the source of this value.
113 */
114 stale: Ref<boolean>;
115 /** Reactive {@link OperationResult.data} for the executed query. */
116 data: Ref<T | undefined>;
117 /** Reactive {@link OperationResult.error} for the executed query. */
118 error: Ref<CombinedError | undefined>;
119 /** Reactive {@link OperationResult.extensions} for the executed query. */
120 extensions: Ref<Record<string, any> | undefined>;
121 /** Reactive {@link Operation} that the current state is for.
122 *
123 * @remarks
124 * This is the {@link Operation} that is currently being executed.
125 * When {@link UseQueryState.fetching} is `true`, this is the
126 * last `Operation` that the current state was for.
127 */
128 operation: Ref<Operation<T, V> | undefined>;
129 /** Indicates whether {@link useQuery} is currently paused.
130 *
131 * @remarks
132 * When `useQuery` has been paused, it will stop receiving updates
133 * from the {@link Client} and won’t execute query operations, until
134 * {@link UseQueryArgs.pause} becomes `true` or {@link UseQueryState.resume}
135 * is called.
136 *
137 * @see {@link https://urql.dev/goto/docs/basics/vue#pausing-usequery} for
138 * documentation on the `pause` option.
139 */
140 isPaused: Ref<boolean>;
141 /** The {@link OperationResult.hasNext} for the executed query. */
142 hasNext: Ref<boolean>;
143 /** Resumes {@link useQuery} if it’s currently paused.
144 *
145 * @remarks
146 * Resumes or starts {@link useQuery}’s query, if it’s currently paused.
147 *
148 * @see {@link https://urql.dev/goto/docs/basics/vue#pausing-usequery} for
149 * documentation on the `pause` option.
150 */
151 resume(): void;
152 /** Pauses {@link useQuery} to stop it from executing the query.
153 *
154 * @remarks
155 * Pauses {@link useQuery}’s query, which stops it from receiving updates
156 * from the {@link Client} and to stop the ongoing query operation.
157 *
158 * @see {@link https://urql.dev/goto/docs/basics/vue#pausing-usequery} for
159 * documentation on the `pause` option.
160 */
161 pause(): void;
162 /** Triggers {@link useQuery} to execute a new GraphQL query operation.
163 *
164 * @param opts - optionally, context options that will be merged with
165 * {@link UseQueryArgs.context} and the `Client`’s options.
166 *
167 * @remarks
168 * When called, {@link useQuery} will re-execute the GraphQL query operation
169 * it currently holds, unless it’s currently paused.
170 *
171 * This is useful for re-executing a query and get a new network result,
172 * by passing a new request policy.
173 *
174 * ```ts
175 * const result = useQuery({ query });
176 *
177 * const refresh = () => {
178 * // Re-execute the query with a network-only policy, skipping the cache
179 * result.executeQuery({ requestPolicy: 'network-only' });
180 * };
181 * ```
182 */
183 executeQuery(opts?: Partial<OperationContext>): UseQueryResponse<T, V>;
184}
185
186/** Return value of {@link useQuery}, which is an awaitable {@link UseQueryState}.
187 *
188 * @remarks
189 * {@link useQuery} returns a {@link UseQueryState} but may also be
190 * awaited inside a Vue `async setup()` function. If it’s awaited
191 * the query is executed before resolving.
192 */
193export type UseQueryResponse<
194 T,
195 V extends AnyVariables = AnyVariables,
196> = UseQueryState<T, V> & PromiseLike<UseQueryState<T, V>>;
197
198/** Function to run a GraphQL query and get reactive GraphQL results.
199 *
200 * @param args - a {@link UseQueryArgs} object, to pass a `query`, `variables`, and options.
201 * @returns a {@link UseQueryResponse} object.
202 *
203 * @remarks
204 * `useQuery` allows GraphQL queries to be defined and executed inside
205 * Vue `setup` functions.
206 * Given {@link UseQueryArgs.query}, it executes the GraphQL query with the
207 * provided {@link Client}.
208 *
209 * The returned result’s reactive values update when the `Client` has
210 * new results for the query, and changes when your input `args` change.
211 *
212 * Additionally, `useQuery` may also be awaited inside an `async setup()`
213 * function to use Vue’s Suspense feature.
214 *
215 * @see {@link https://urql.dev/goto/docs/basics/vue#queries} for `useQuery` docs.
216 *
217 * @example
218 * ```ts
219 * import { gql, useQuery } from '@urql/vue';
220 *
221 * const TodosQuery = gql`
222 * query { todos { id, title } }
223 * `;
224 *
225 * export default {
226 * setup() {
227 * const result = useQuery({ query: TodosQuery });
228 * return { data: result.data };
229 * },
230 * };
231 * ```
232 */
233export function useQuery<T = any, V extends AnyVariables = AnyVariables>(
234 args: UseQueryArgs<T, V>
235): UseQueryResponse<T, V> {
236 return callUseQuery(args);
237}
238
239export function callUseQuery<T = any, V extends AnyVariables = AnyVariables>(
240 args: UseQueryArgs<T, V>,
241 client: Ref<Client> = useClient(),
242 stops?: WatchStopHandle[]
243): UseQueryResponse<T, V> {
244 const data: Ref<T | undefined> = shallowRef();
245
246 const { fetching, operation, extensions, stale, error, hasNext } =
247 useRequestState<T, V>();
248
249 const { isPaused, source, pause, resume, execute, teardown } = useClientState<
250 T,
251 V
252 >(args, client, 'executeQuery');
253
254 const teardownQuery = watchEffect(
255 onInvalidate => {
256 if (source.value) {
257 fetching.value = true;
258 stale.value = false;
259
260 onInvalidate(
261 pipe(
262 source.value,
263 onEnd(() => {
264 fetching.value = false;
265 stale.value = false;
266 hasNext.value = false;
267 }),
268 subscribe(res => {
269 data.value = res.data;
270 stale.value = !!res.stale;
271 fetching.value = false;
272 error.value = res.error;
273 operation.value = res.operation;
274 extensions.value = res.extensions;
275 hasNext.value = res.hasNext;
276 })
277 ).unsubscribe
278 );
279 } else {
280 fetching.value = false;
281 stale.value = false;
282 hasNext.value = false;
283 }
284 },
285 {
286 // NOTE: This part of the query pipeline is only initialised once and will need
287 // to do so synchronously
288 flush: 'sync',
289 }
290 );
291
292 stops && stops.push(teardown, teardownQuery);
293
294 const then: UseQueryResponse<T, V>['then'] = (onFulfilled, onRejected) => {
295 let sub: Subscription | void;
296
297 const promise = new Promise<UseQueryState<T, V>>(resolve => {
298 if (!source.value) {
299 return resolve(state);
300 }
301 let hasResult = false;
302 sub = pipe(
303 source.value,
304 subscribe(() => {
305 if (!state.fetching.value && !state.stale.value) {
306 if (sub) sub.unsubscribe();
307 hasResult = true;
308 resolve(state);
309 }
310 })
311 );
312 if (hasResult) sub.unsubscribe();
313 });
314
315 return promise.then(onFulfilled, onRejected);
316 };
317
318 const state: UseQueryState<T, V> = {
319 data,
320 stale,
321 error,
322 operation,
323 extensions,
324 fetching,
325 isPaused,
326 hasNext,
327 pause,
328 resume,
329 executeQuery(opts?: Partial<OperationContext>): UseQueryResponse<T, V> {
330 execute(opts);
331 return { ...state, then };
332 },
333 };
334
335 return { ...state, then };
336}