UNPKG

@preact-signals/query

Version:

A reactive utility for React/Preact that simplifies the handling of data fetching and state management. Powered by Preact Signals, it provides hooks and functions to create reactive resources and manage their state seamlessly.

172 lines (159 loc) 4.97 kB
import { useComputedOnce, useSignalEffectOnce, useSignalOfReactive, } from "@preact-signals/utils/hooks"; import type { QueryKey, QueryObserver } from "@tanstack/query-core"; import { useMemo } from "react"; import { useQueryClient$ } from "./react-query/QueryClientProvider"; import { useQueryErrorResetBoundary$ } from "./react-query/QueryErrorResetBoundary"; import { ensurePreventErrorBoundaryRetry, getHasError, useClearResetErrorBoundary$, } from "./react-query/errorBoundaryUtils"; import { useIsRestoring$ } from "./react-query/isRestoring"; import { ensureStaleTime, shouldSuspend } from "./react-query/suspense"; import { StaticBaseQueryOptions, UseBaseQueryResult$ } from "./types"; import { useObserverStore } from "./useObserver"; import { useRefBasedOptions, wrapFunctionsInUntracked } from "./utils"; import { untracked } from "@preact-signals/unified-signals"; import { $ } from "@preact-signals/utils"; const enum ReturnStatus { Error, Success, Suspense, } export const createBaseQuery = (Observer: typeof QueryObserver) => < TQueryFnData, TError, TData, TQueryData, TQueryKey extends QueryKey = QueryKey, >( options: () => StaticBaseQueryOptions< TQueryFnData, TError, TData, TQueryData, TQueryKey > ): UseBaseQueryResult$<TData, TError> => { const $options = useRefBasedOptions(options); const $queryClient = useQueryClient$({ context: useComputedOnce(() => $options.value.context).value, }); const $isRestoring = useIsRestoring$(); const $errorBoundary = useQueryErrorResetBoundary$(); const $suspenseBehavior = $( () => $options.value.suspenseBehavior ?? "load-on-access" ); const $defaultedOptions = useComputedOnce(() => { const defaulted = wrapFunctionsInUntracked( $queryClient.value.defaultQueryOptions($options.value) ); defaulted._optimisticResults = $isRestoring.value ? "isRestoring" : "optimistic"; ensureStaleTime(defaulted); ensurePreventErrorBoundaryRetry(defaulted, $errorBoundary.value); return defaulted; }); const $observer = useComputedOnce( () => new Observer($queryClient.value, $defaultedOptions.peek()) ); useSignalEffectOnce(() => { $observer.value.setOptions($defaultedOptions.value); }); const state = useObserverStore(() => ({ getCurrent: () => $observer.value.getOptimisticResult( $defaultedOptions.value ) as UseBaseQueryResult$<TData, TError>, subscribe: (emit) => $observer.value.subscribe((newValue) => { emit(newValue as UseBaseQueryResult$<TData, TError>); }), })); useClearResetErrorBoundary$($errorBoundary); const $shouldSuspend = $(() => shouldSuspend($defaultedOptions.value, state, $isRestoring.value) ); const getData = () => { if ( getHasError({ result: state, errorResetBoundary: $errorBoundary.value, query: $observer.value.getCurrentQuery(), useErrorBoundary: $defaultedOptions.value.useErrorBoundary, }) ) { return { type: ReturnStatus.Error, data: state.error, } as const; } if ($shouldSuspend.value) { // will not refetch if already fetching // should suspend is not using data, so all will work fine return { type: ReturnStatus.Suspense, data: $observer.value.fetchOptimistic($defaultedOptions.value), } as const; } return { type: ReturnStatus.Success, data: state.data, } as const; }; const dataComputed = useComputedOnce(() => { const res = getData(); if (res.type === ReturnStatus.Success) { return res.data; } throw res.data; }); untracked(() => { if ( $shouldSuspend.value && $suspenseBehavior.value !== "load-on-access" ) { const data = getData(); if ( data.type === ReturnStatus.Suspense && $suspenseBehavior.value === "suspend-eagerly" ) { throw data.data; } } }); const willSuspendOrThrow = useComputedOnce(() => { if ( !$shouldSuspend.value || $suspenseBehavior.value !== "suspend-eagerly" ) { return false; } return getData().type !== ReturnStatus.Success; }); willSuspendOrThrow.value; // @ts-expect-error actually it can be written state.dataSafe = undefined; return useMemo( () => new Proxy(state, { get(target, prop) { if (prop === "data") { return dataComputed.value; } if (prop === "dataSafe") { return target.data; } // @ts-expect-error return Reflect.get(...arguments); }, }), [] ); };