import { useEffect, useCallback, useMemo } from 'preact/hooks'; import type { Source } from 'wonka'; import { pipe, share, takeWhile, concat, fromValue, switchMap, map, scan, } from 'wonka'; import type { Client, GraphQLRequestParams, AnyVariables, CombinedError, OperationContext, RequestPolicy, OperationResult, Operation, } from '@urql/core'; import { useClient } from '../context'; import { useSource } from './useSource'; import { useRequest } from './useRequest'; import { initialState } from './constants'; /** Input arguments for the {@link useQuery} hook. * * @param query - The GraphQL query that `useQuery` executes. * @param variables - The variables for the GraphQL query that `useQuery` executes. */ export type UseQueryArgs< Variables extends AnyVariables = AnyVariables, Data = any, > = { /** Updates the {@link RequestPolicy} for the executed GraphQL query operation. * * @remarks * `requestPolicy` modifies the {@link RequestPolicy} of the GraphQL query operation * that `useQuery` executes, and indicates a caching strategy for cache exchanges. * * For example, when set to `'cache-and-network'`, {@link useQuery} will * receive a cached result with `stale: true` and an API request will be * sent in the background. * * @see {@link OperationContext.requestPolicy} for where this value is set. */ requestPolicy?: RequestPolicy; /** Updates the {@link OperationContext} for the executed GraphQL query operation. * * @remarks * `context` may be passed to {@link useQuery}, to update the {@link OperationContext} * of a query operation. This may be used to update the `context` that exchanges * will receive for a single hook. * * Hint: This should be wrapped in a `useMemo` hook, to make sure that your * component doesn’t infinitely update. * * @example * ```ts * const [result, reexecute] = useQuery({ * query, * context: useMemo(() => ({ * additionalTypenames: ['Item'], * }), []) * }); * ``` */ context?: Partial; /** Prevents {@link useQuery} from automatically executing GraphQL query operations. * * @remarks * `pause` may be set to `true` to stop {@link useQuery} from executing * automatically. The hook will stop receiving updates from the {@link Client} * and won’t execute the query operation, until either it’s set to `false` * or the {@link UseQueryExecute} function is called. * * @see {@link https://urql.dev/goto/docs/basics/react-preact/#pausing-usequery} for * documentation on the `pause` option. */ pause?: boolean; } & GraphQLRequestParams; /** State of the current query, your {@link useQuery} hook is executing. * * @remarks * `UseQueryState` is returned (in a tuple) by {@link useQuery} and * gives you the updating {@link OperationResult} of GraphQL queries. * * Even when the query and variables passed to {@link useQuery} change, * this state preserves the prior state and sets the `fetching` flag to * `true`. * This allows you to display the previous state, while implementing * a separate loading indicator separately. */ export interface UseQueryState< Data = any, Variables extends AnyVariables = AnyVariables, > { /** Indicates whether `useQuery` is waiting for a new result. * * @remarks * When `useQuery` is passed a new query and/or variables, it will * start executing the new query operation and `fetching` is set to * `true` until a result arrives. * * Hint: This is subtly different than whether the query is actually * fetching, and doesn’t indicate whether a query is being re-executed * in the background. For this, see {@link UseQueryState.stale}. */ fetching: boolean; /** Indicates that the state is not fresh and a new result will follow. * * @remarks * The `stale` flag is set to `true` when a new result for the query * is expected and `useQuery` is waiting for it. This may indicate that * a new request is being requested in the background. * * @see {@link OperationResult.stale} for the source of this value. */ stale: boolean; /** The {@link OperationResult.data} for the executed query. */ data?: Data; /** The {@link OperationResult.error} for the executed query. */ error?: CombinedError; /** The {@link OperationResult.extensions} for the executed query. */ extensions?: Record; /** The {@link Operation} that the current state is for. * * @remarks * This is the {@link Operation} that is currently being executed. * When {@link UseQueryState.fetching} is `true`, this is the * last `Operation` that the current state was for. */ operation?: Operation; /** The {@link OperationResult.hasNext} for the executed query. */ hasNext: boolean; } /** Triggers {@link useQuery} to execute a new GraphQL query operation. * * @remarks * When called, {@link useQuery} will re-execute the GraphQL query operation * it currently holds, even if {@link UseQueryArgs.pause} is set to `true`. * * This is useful for executing a paused query or re-executing a query * and get a new network result, by passing a new request policy. * * ```ts * const [result, reexecuteQuery] = useQuery({ query }); * * const refresh = () => { * // Re-execute the query with a network-only policy, skipping the cache * reexecuteQuery({ requestPolicy: 'network-only' }); * }; * ``` */ export type UseQueryExecute = (opts?: Partial) => void; /** Result tuple returned by the {@link useQuery} hook. * * @remarks * Similarly to a `useState` hook’s return value, * the first element is the {@link useQuery}’s result and state, * a {@link UseQueryState} object, * and the second is used to imperatively re-execute the query * via a {@link UseQueryExecute} function. */ export type UseQueryResponse< Data = any, Variables extends AnyVariables = AnyVariables, > = [UseQueryState, UseQueryExecute]; /** Convert the Source to a React Suspense source on demand * @internal */ function toSuspenseSource(source: Source): Source { const shared = share(source); let cache: T | void; let resolve: (value: T) => void; return sink => { let hasSuspended = false; pipe( shared, takeWhile(result => { // The first result that is received will resolve the suspense // promise after waiting for a microtick if (cache === undefined) Promise.resolve(result).then(resolve); cache = result; return !hasSuspended; }) )(sink); // If we haven't got a previous result then start suspending // otherwise issue the last known result immediately if (cache !== undefined) { const signal = [cache] as [T] & { tag: 1 }; signal.tag = 1; sink(signal); } else { hasSuspended = true; sink(0 /* End */); throw new Promise(_resolve => { resolve = _resolve; }); } }; } const isSuspense = (client: Client, context?: Partial) => context && context.suspense !== undefined ? !!context.suspense : client.suspense; const sources = new Map>(); /** Hook to run a GraphQL query and get updated GraphQL results. * * @param args - a {@link UseQueryArgs} object, to pass a `query`, `variables`, and options. * @returns a {@link UseQueryResponse} tuple of a {@link UseQueryState} result, and re-execute function. * * @remarks * `useQuery` allows GraphQL queries to be defined and executed. * Given {@link UseQueryArgs.query}, it executes the GraphQL query with the * context’s {@link Client}. * * The returned result updates when the `Client` has new results * for the query, and changes when your input `args` change. * * Additionally, if the `suspense` option is enabled on the `Client`, * the `useQuery` hook will suspend instead of indicating that it’s * waiting for a result via {@link UseQueryState.fetching}. * * @see {@link https://urql.dev/goto/docs/basics/react-preact/#queries} for `useQuery` docs. * * @example * ```ts * import { gql, useQuery } from '@urql/preact'; * * const TodosQuery = gql` * query { todos { id, title } } * `; * * const Todos = () => { * const [result, reexecuteQuery] = useQuery({ * query: TodosQuery, * variables: {}, * }); * // ... * }; * ``` */ export function useQuery< Data = any, Variables extends AnyVariables = AnyVariables, >(args: UseQueryArgs): UseQueryResponse { const client = useClient(); // This creates a request which will keep a stable reference // if request.key doesn't change const request = useRequest(args.query, args.variables as Variables); // Create a new query-source from client.executeQuery const makeQuery$ = useCallback( (opts?: Partial) => { // Determine whether suspense is enabled for the given operation const suspense = isSuspense(client, args.context); let source: Source | void = suspense ? sources.get(request.key) : undefined; if (!source) { source = client.executeQuery(request, { requestPolicy: args.requestPolicy, ...args.context, ...opts, }); // Create a suspense source and cache it for the given request if (suspense) { source = toSuspenseSource(source); if (typeof window !== 'undefined') { sources.set(request.key, source); } } } return source; }, [client, request, args.requestPolicy, args.context] ); const query$ = useMemo(() => { return args.pause ? null : makeQuery$(); }, [args.pause, makeQuery$]); const [state, update] = useSource( query$, useCallback((query$$, prevState?: UseQueryState) => { return pipe( query$$, switchMap(query$ => { if (!query$) return fromValue({ fetching: false, stale: false, hasNext: false }); return concat([ // Initially set fetching to true fromValue({ fetching: true, stale: false }), pipe( query$, map(({ stale, data, error, extensions, operation, hasNext }) => ({ fetching: false, stale: !!stale, hasNext, data, error, operation, extensions, })) ), // When the source proactively closes, fetching is set to false fromValue({ fetching: false, stale: false, hasNext: false }), ]); }), // The individual partial results are merged into each previous result scan( (result: UseQueryState, partial) => ({ ...result, ...partial, }), prevState || initialState ) ); }, []) ); // This is the imperative execute function passed to the user const executeQuery = useCallback( (opts?: Partial) => { update(makeQuery$({ suspense: false, ...opts })); }, [update, makeQuery$] ); useEffect(() => { sources.delete(request.key); // Delete any cached suspense source if (!isSuspense(client, args.context)) update(query$); }, [update, client, query$, request, args.context]); if (isSuspense(client, args.context)) { update(query$); } return [state, executeQuery]; }