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 } }]) => (
* <>
*
* >
* )}
*
* );
* };
* ```
*
* @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;