Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.
at main 12 kB view raw
1import { useEffect, useCallback, useMemo } from 'preact/hooks'; 2 3import type { Source } from 'wonka'; 4import { 5 pipe, 6 share, 7 takeWhile, 8 concat, 9 fromValue, 10 switchMap, 11 map, 12 scan, 13} from 'wonka'; 14 15import type { 16 Client, 17 GraphQLRequestParams, 18 AnyVariables, 19 CombinedError, 20 OperationContext, 21 RequestPolicy, 22 OperationResult, 23 Operation, 24} from '@urql/core'; 25 26import { useClient } from '../context'; 27import { useSource } from './useSource'; 28import { useRequest } from './useRequest'; 29import { initialState } from './constants'; 30 31/** Input arguments for the {@link useQuery} hook. 32 * 33 * @param query - The GraphQL query that `useQuery` executes. 34 * @param variables - The variables for the GraphQL query that `useQuery` executes. 35 */ 36export type UseQueryArgs< 37 Variables extends AnyVariables = AnyVariables, 38 Data = any, 39> = { 40 /** Updates the {@link RequestPolicy} for the executed GraphQL query operation. 41 * 42 * @remarks 43 * `requestPolicy` modifies the {@link RequestPolicy} of the GraphQL query operation 44 * that `useQuery` executes, and indicates a caching strategy for cache exchanges. 45 * 46 * For example, when set to `'cache-and-network'`, {@link useQuery} will 47 * receive a cached result with `stale: true` and an API request will be 48 * sent in the background. 49 * 50 * @see {@link OperationContext.requestPolicy} for where this value is set. 51 */ 52 requestPolicy?: RequestPolicy; 53 /** Updates the {@link OperationContext} for the executed GraphQL query operation. 54 * 55 * @remarks 56 * `context` may be passed to {@link useQuery}, to update the {@link OperationContext} 57 * of a query operation. This may be used to update the `context` that exchanges 58 * will receive for a single hook. 59 * 60 * Hint: This should be wrapped in a `useMemo` hook, to make sure that your 61 * component doesn’t infinitely update. 62 * 63 * @example 64 * ```ts 65 * const [result, reexecute] = useQuery({ 66 * query, 67 * context: useMemo(() => ({ 68 * additionalTypenames: ['Item'], 69 * }), []) 70 * }); 71 * ``` 72 */ 73 context?: Partial<OperationContext>; 74 /** Prevents {@link useQuery} from automatically executing GraphQL query operations. 75 * 76 * @remarks 77 * `pause` may be set to `true` to stop {@link useQuery} from executing 78 * automatically. The hook will stop receiving updates from the {@link Client} 79 * and won’t execute the query operation, until either it’s set to `false` 80 * or the {@link UseQueryExecute} function is called. 81 * 82 * @see {@link https://urql.dev/goto/docs/basics/react-preact/#pausing-usequery} for 83 * documentation on the `pause` option. 84 */ 85 pause?: boolean; 86} & GraphQLRequestParams<Data, Variables>; 87 88/** State of the current query, your {@link useQuery} hook is executing. 89 * 90 * @remarks 91 * `UseQueryState` is returned (in a tuple) by {@link useQuery} and 92 * gives you the updating {@link OperationResult} of GraphQL queries. 93 * 94 * Even when the query and variables passed to {@link useQuery} change, 95 * this state preserves the prior state and sets the `fetching` flag to 96 * `true`. 97 * This allows you to display the previous state, while implementing 98 * a separate loading indicator separately. 99 */ 100export interface UseQueryState< 101 Data = any, 102 Variables extends AnyVariables = AnyVariables, 103> { 104 /** Indicates whether `useQuery` is waiting for a new result. 105 * 106 * @remarks 107 * When `useQuery` is passed a new query and/or variables, it will 108 * start executing the new query operation and `fetching` is set to 109 * `true` until a result arrives. 110 * 111 * Hint: This is subtly different than whether the query is actually 112 * fetching, and doesn’t indicate whether a query is being re-executed 113 * in the background. For this, see {@link UseQueryState.stale}. 114 */ 115 fetching: boolean; 116 /** Indicates that the state is not fresh and a new result will follow. 117 * 118 * @remarks 119 * The `stale` flag is set to `true` when a new result for the query 120 * is expected and `useQuery` is waiting for it. This may indicate that 121 * a new request is being requested in the background. 122 * 123 * @see {@link OperationResult.stale} for the source of this value. 124 */ 125 stale: boolean; 126 /** The {@link OperationResult.data} for the executed query. */ 127 data?: Data; 128 /** The {@link OperationResult.error} for the executed query. */ 129 error?: CombinedError; 130 /** The {@link OperationResult.extensions} for the executed query. */ 131 extensions?: Record<string, any>; 132 /** The {@link Operation} that the current state is for. 133 * 134 * @remarks 135 * This is the {@link Operation} that is currently being executed. 136 * When {@link UseQueryState.fetching} is `true`, this is the 137 * last `Operation` that the current state was for. 138 */ 139 operation?: Operation<Data, Variables>; 140 /** The {@link OperationResult.hasNext} for the executed query. */ 141 hasNext: boolean; 142} 143 144/** Triggers {@link useQuery} to execute a new GraphQL query operation. 145 * 146 * @remarks 147 * When called, {@link useQuery} will re-execute the GraphQL query operation 148 * it currently holds, even if {@link UseQueryArgs.pause} is set to `true`. 149 * 150 * This is useful for executing a paused query or re-executing a query 151 * and get a new network result, by passing a new request policy. 152 * 153 * ```ts 154 * const [result, reexecuteQuery] = useQuery({ query }); 155 * 156 * const refresh = () => { 157 * // Re-execute the query with a network-only policy, skipping the cache 158 * reexecuteQuery({ requestPolicy: 'network-only' }); 159 * }; 160 * ``` 161 */ 162export type UseQueryExecute = (opts?: Partial<OperationContext>) => void; 163 164/** Result tuple returned by the {@link useQuery} hook. 165 * 166 * @remarks 167 * Similarly to a `useState` hook’s return value, 168 * the first element is the {@link useQuery}’s result and state, 169 * a {@link UseQueryState} object, 170 * and the second is used to imperatively re-execute the query 171 * via a {@link UseQueryExecute} function. 172 */ 173export type UseQueryResponse< 174 Data = any, 175 Variables extends AnyVariables = AnyVariables, 176> = [UseQueryState<Data, Variables>, UseQueryExecute]; 177 178/** Convert the Source to a React Suspense source on demand 179 * @internal 180 */ 181function toSuspenseSource<T>(source: Source<T>): Source<T> { 182 const shared = share(source); 183 let cache: T | void; 184 let resolve: (value: T) => void; 185 186 return sink => { 187 let hasSuspended = false; 188 189 pipe( 190 shared, 191 takeWhile(result => { 192 // The first result that is received will resolve the suspense 193 // promise after waiting for a microtick 194 if (cache === undefined) Promise.resolve(result).then(resolve); 195 cache = result; 196 return !hasSuspended; 197 }) 198 )(sink); 199 200 // If we haven't got a previous result then start suspending 201 // otherwise issue the last known result immediately 202 if (cache !== undefined) { 203 const signal = [cache] as [T] & { tag: 1 }; 204 signal.tag = 1; 205 sink(signal); 206 } else { 207 hasSuspended = true; 208 sink(0 /* End */); 209 throw new Promise<T>(_resolve => { 210 resolve = _resolve; 211 }); 212 } 213 }; 214} 215 216const isSuspense = (client: Client, context?: Partial<OperationContext>) => 217 context && context.suspense !== undefined 218 ? !!context.suspense 219 : client.suspense; 220 221const sources = new Map<number, Source<OperationResult>>(); 222 223/** Hook to run a GraphQL query and get updated GraphQL results. 224 * 225 * @param args - a {@link UseQueryArgs} object, to pass a `query`, `variables`, and options. 226 * @returns a {@link UseQueryResponse} tuple of a {@link UseQueryState} result, and re-execute function. 227 * 228 * @remarks 229 * `useQuery` allows GraphQL queries to be defined and executed. 230 * Given {@link UseQueryArgs.query}, it executes the GraphQL query with the 231 * context’s {@link Client}. 232 * 233 * The returned result updates when the `Client` has new results 234 * for the query, and changes when your input `args` change. 235 * 236 * Additionally, if the `suspense` option is enabled on the `Client`, 237 * the `useQuery` hook will suspend instead of indicating that it’s 238 * waiting for a result via {@link UseQueryState.fetching}. 239 * 240 * @see {@link https://urql.dev/goto/docs/basics/react-preact/#queries} for `useQuery` docs. 241 * 242 * @example 243 * ```ts 244 * import { gql, useQuery } from '@urql/preact'; 245 * 246 * const TodosQuery = gql` 247 * query { todos { id, title } } 248 * `; 249 * 250 * const Todos = () => { 251 * const [result, reexecuteQuery] = useQuery({ 252 * query: TodosQuery, 253 * variables: {}, 254 * }); 255 * // ... 256 * }; 257 * ``` 258 */ 259export function useQuery< 260 Data = any, 261 Variables extends AnyVariables = AnyVariables, 262>(args: UseQueryArgs<Variables, Data>): UseQueryResponse<Data, Variables> { 263 const client = useClient(); 264 // This creates a request which will keep a stable reference 265 // if request.key doesn't change 266 const request = useRequest(args.query, args.variables as Variables); 267 268 // Create a new query-source from client.executeQuery 269 const makeQuery$ = useCallback( 270 (opts?: Partial<OperationContext>) => { 271 // Determine whether suspense is enabled for the given operation 272 const suspense = isSuspense(client, args.context); 273 let source: Source<OperationResult> | void = suspense 274 ? sources.get(request.key) 275 : undefined; 276 277 if (!source) { 278 source = client.executeQuery(request, { 279 requestPolicy: args.requestPolicy, 280 ...args.context, 281 ...opts, 282 }); 283 284 // Create a suspense source and cache it for the given request 285 if (suspense) { 286 source = toSuspenseSource(source); 287 if (typeof window !== 'undefined') { 288 sources.set(request.key, source); 289 } 290 } 291 } 292 293 return source; 294 }, 295 [client, request, args.requestPolicy, args.context] 296 ); 297 298 const query$ = useMemo(() => { 299 return args.pause ? null : makeQuery$(); 300 }, [args.pause, makeQuery$]); 301 302 const [state, update] = useSource( 303 query$, 304 useCallback((query$$, prevState?: UseQueryState<Data, Variables>) => { 305 return pipe( 306 query$$, 307 switchMap(query$ => { 308 if (!query$) 309 return fromValue({ fetching: false, stale: false, hasNext: false }); 310 311 return concat([ 312 // Initially set fetching to true 313 fromValue({ fetching: true, stale: false }), 314 pipe( 315 query$, 316 map(({ stale, data, error, extensions, operation, hasNext }) => ({ 317 fetching: false, 318 stale: !!stale, 319 hasNext, 320 data, 321 error, 322 operation, 323 extensions, 324 })) 325 ), 326 // When the source proactively closes, fetching is set to false 327 fromValue({ fetching: false, stale: false, hasNext: false }), 328 ]); 329 }), 330 // The individual partial results are merged into each previous result 331 scan( 332 (result: UseQueryState<Data, Variables>, partial) => ({ 333 ...result, 334 ...partial, 335 }), 336 prevState || initialState 337 ) 338 ); 339 }, []) 340 ); 341 342 // This is the imperative execute function passed to the user 343 const executeQuery = useCallback( 344 (opts?: Partial<OperationContext>) => { 345 update(makeQuery$({ suspense: false, ...opts })); 346 }, 347 [update, makeQuery$] 348 ); 349 350 useEffect(() => { 351 sources.delete(request.key); // Delete any cached suspense source 352 if (!isSuspense(client, args.context)) update(query$); 353 }, [update, client, query$, request, args.context]); 354 355 if (isSuspense(client, args.context)) { 356 update(query$); 357 } 358 359 return [state, executeQuery]; 360}