import type { QueryObserverSuccessResult, UseQueryResult, } from "@tanstack/react-query"; import * as React from "react"; import type { FallbackProps } from "react-error-boundary"; import { isQueryObserverLoadingErrorResult, isQueryObserverLoadingResult, } from "./helpers"; import type { ErrorFallbackProps } from "./types"; /** * Provides a type for the children function of `` * that allows the types of a successful query to be inferred for ease of use * @example * ```ts * type C = ChildrenFn, * UseQueryResult, * ]>; * // Equivalent to * type C = (queries: readonly [ * QueryObserverSuccessResult, * QueryObserverSuccessResult, * ]) => React.ReactNode; * ``` */ type ChildrenFn> = (queries: { [Key in keyof TQueries]: TQueries[Key] extends UseQueryResult< infer TData, infer TError > ? QueryObserverSuccessResult : TQueries[Key]; }) => React.ReactNode; export type QueriesBoundaryProps< TQueries extends ReadonlyArray, > = { children: React.ReactNode | ChildrenFn; loadingFallback: React.ReactNode; queries: TQueries; } & ErrorFallbackProps; export class AggregateQueriesError extends AggregateError {} /** * Utility component for managing the loading and error states for multiple React Query queries. Specifying the queries * automatically infers the types of the success state of the queries in the render prop children (if used). * * There are three mutually exclusive options for specifying an error fallback: * - `errorFallback` - JSX element * - `errorFallbackRender` - a function which takes `FallbackProps` and renders JSX * - `ErrorFallbackComponent` - a React component which takes `FallbackProps` * * @example * ```tsx * const MyComponent = () => { * const query1: UseQueryResult<{ items: readonly string[] }> = useMyQuery1(); * const query2: UseQueryResult<{ balance: number }> = useMyQuery2(); * * return ( * ( * <> * Something went wrong.{" "} * * * )} * loadingFallback={} * queries={[query1, query2]} * > * {([{ data: { items } }, { data: { balance } }]) => ( * <> *

* Your balance:{" "} * {new Intl.NumberFormat("en-AU", { style: "currency", currency: "AUD" }).format(balance)} *

*

* You have:{" "} * {new Intl.ListFormat("en", { style: "long", type: "conunction" }).format(data.items)} *

* * )} * * ); * }; * ``` * * @param props */ const QueriesBoundary = < // Including a newline here to fix broken syntax highlighting const TQueries extends ReadonlyArray, >({ children, errorFallback, errorFallbackRender, ErrorFallbackComponent, loadingFallback, queries, }: QueriesBoundaryProps): React.JSX.Element => { /** * If any of the queries are in the error state, combine them into a single `AggregateError` */ const error = React.useMemo(() => { const errors = queries .filter(isQueryObserverLoadingErrorResult) .map((query) => query.error); return errors.length ? new AggregateQueriesError( errors, "One or more queries are in the LoadingErrorResult state:", ) : undefined; }, [queries]); /** * If any of the queries have a status of `"error"`, this will trigger a refetch of each */ const resetErrorBoundary = React.useCallback( () => queries .filter( (query): query is Extract => query.status === "error", ) .forEach((query) => void query.refetch()), [queries], ); if (error) { if (React.isValidElement(errorFallback)) { return <>{errorFallback}; } const errorFallbackProps: FallbackProps = { error, resetErrorBoundary, }; if (typeof errorFallbackRender === "function") { return <>{errorFallbackRender(errorFallbackProps)}; } if (ErrorFallbackComponent) { return ; } /* istanbul ignore next */ throw new Error( "QueriesBoundary requires either errorFallback, errorFallbackRender, or ErrorFallbackComponent prop", ); } if (queries.some((query) => isQueryObserverLoadingResult(query))) { return <>{loadingFallback}; } return ( <> {typeof children === "function" ? children(queries as Parameters>[0]) : children} ); }; export default QueriesBoundary;