Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.
at main 11 kB view raw
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}