From ee0ac078cfe6eb0ab3e2a177a87518d9008283e6 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Mon, 18 Jul 2022 10:49:46 -0400 Subject: [PATCH 01/14] chore: remove Deferrable/ResolvedDeferrable types for raw Promises --- examples/data-router/src/App.tsx | 11 +++++------ packages/react-router-dom/index.tsx | 1 - packages/react-router-native/index.tsx | 1 - packages/react-router/index.ts | 3 +-- packages/react-router/lib/components.tsx | 3 +-- packages/react-router/lib/hooks.tsx | 11 +---------- 6 files changed, 8 insertions(+), 22 deletions(-) diff --git a/examples/data-router/src/App.tsx b/examples/data-router/src/App.tsx index 37f564b130..57d27e5cf2 100644 --- a/examples/data-router/src/App.tsx +++ b/examples/data-router/src/App.tsx @@ -1,7 +1,6 @@ import React from "react"; import { type ActionFunction, - type Deferrable, type LoaderFunction, DataBrowserRouter, Deferred, @@ -238,11 +237,11 @@ function Todo() { interface DeferredRouteLoaderData { critical1: string; critical2: string; - lazyResolved: Deferrable; - lazy1: Deferrable; - lazy2: Deferrable; - lazy3: Deferrable; - lazyError: Deferrable; + lazyResolved: Promise; + lazy1: Promise; + lazy2: Promise; + lazy3: Promise; + lazyError: Promise; } const rand = () => Math.round(Math.random() * 100); diff --git a/packages/react-router-dom/index.tsx b/packages/react-router-dom/index.tsx index 5a45117932..1baec91ad0 100644 --- a/packages/react-router-dom/index.tsx +++ b/packages/react-router-dom/index.tsx @@ -63,7 +63,6 @@ export type { ActionFunctionArgs, DataMemoryRouterProps, DataRouteMatch, - Deferrable, DeferredProps, Fetcher, Hash, diff --git a/packages/react-router-native/index.tsx b/packages/react-router-native/index.tsx index 699ca3091c..e50285341e 100644 --- a/packages/react-router-native/index.tsx +++ b/packages/react-router-native/index.tsx @@ -27,7 +27,6 @@ export type { ActionFunctionArgs, DataMemoryRouterProps, DataRouteMatch, - Deferrable, DeferredProps, Fetcher, Hash, diff --git a/packages/react-router/index.ts b/packages/react-router/index.ts index 6eca1ddd64..a0cab90224 100644 --- a/packages/react-router/index.ts +++ b/packages/react-router/index.ts @@ -68,7 +68,7 @@ import { NavigationContext, RouteContext, } from "./lib/context"; -import type { Deferrable, NavigateFunction } from "./lib/hooks"; +import type { NavigateFunction } from "./lib/hooks"; import { useHref, useInRouterContext, @@ -102,7 +102,6 @@ export type { ActionFunctionArgs, DataMemoryRouterProps, DataRouteMatch, - Deferrable, DeferredProps, Fetcher, Hash, diff --git a/packages/react-router/lib/components.tsx b/packages/react-router/lib/components.tsx index 1e83aa42bf..72a2619ac8 100644 --- a/packages/react-router/lib/components.tsx +++ b/packages/react-router/lib/components.tsx @@ -30,7 +30,6 @@ import { DataRouterStateContext, DeferredContext, } from "./context"; -import type { ResolvedDeferrable } from "./hooks"; import { useDeferredData, useInRouterContext, @@ -431,7 +430,7 @@ function DataRoutes({ } export interface DeferredResolveRenderFunction { - (data: ResolvedDeferrable): JSX.Element; + (data: Awaited): JSX.Element; } export interface DeferredProps diff --git a/packages/react-router/lib/hooks.tsx b/packages/react-router/lib/hooks.tsx index df8cec4dc3..1bdd07487b 100644 --- a/packages/react-router/lib/hooks.tsx +++ b/packages/react-router/lib/hooks.tsx @@ -724,21 +724,12 @@ export function useRouteError() { return state.errors?.[thisRoute.route.id]; } -export type Deferrable = never | T | Promise; -export type ResolvedDeferrable = T extends null | undefined - ? T - : T extends Deferrable - ? T2 extends Promise - ? T3 - : T2 - : T; - /** * Returns the happy-path data from the nearest ancestor value */ export function useDeferredData() { let value = React.useContext(DeferredContext); - return value as ResolvedDeferrable; + return value as Awaited; } const alreadyWarned: Record = {}; From 55ffecf5392391ebe6b76cd73c9971d5f75e04d3 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Mon, 18 Jul 2022 12:41:07 -0400 Subject: [PATCH 02/14] feat: support deferred arrays --- packages/router/__tests__/router-test.ts | 48 +++++++++++++++++++- packages/router/router.ts | 9 +--- packages/router/utils.ts | 56 ++++++++++++++++-------- 3 files changed, 86 insertions(+), 27 deletions(-) diff --git a/packages/router/__tests__/router-test.ts b/packages/router/__tests__/router-test.ts index 3ff909b4d8..3453e67185 100644 --- a/packages/router/__tests__/router-test.ts +++ b/packages/router/__tests__/router-test.ts @@ -7162,7 +7162,7 @@ describe("a router", () => { }); describe("deferred", () => { - it("should support returning deferred responses", async () => { + it("should support returning deferred responses (object)", async () => { let t = setup({ routes: [ { @@ -7226,6 +7226,52 @@ describe("a router", () => { }); }); + it("should support returning deferred responses (array)", async () => { + let t = setup({ + routes: [ + { + id: "index", + index: true, + }, + { + id: "lazy", + path: "lazy", + loader: true, + }, + ], + initialEntries: ["/"], + }); + + let A = await t.navigate("/lazy"); + + let dfd1 = defer(); + let dfd2 = defer(); + let dfd3 = defer(); + dfd1.resolve("Immediate data"); + await A.loaders.lazy.resolve( + deferred(["1", "2", dfd1.promise, dfd2.promise, dfd3.promise]) + ); + expect(t.router.state.loaderData).toEqual({ + lazy: [ + "1", + "2", + "Immediate data", + expect.any(Promise), + expect.any(Promise), + ], + }); + + await dfd2.resolve("2"); + expect(t.router.state.loaderData).toEqual({ + lazy: ["1", "2", "Immediate data", "2", expect.any(Promise)], + }); + + await dfd3.resolve("3"); + expect(t.router.state.loaderData).toEqual({ + lazy: ["1", "2", "Immediate data", "2", "3"], + }); + }); + it("should cancel outstanding deferreds on a new navigation", async () => { let t = setup({ routes: [ diff --git a/packages/router/router.ts b/packages/router/router.ts index 485fdc2d41..7d800a8b8b 100644 --- a/packages/router/router.ts +++ b/packages/router/router.ts @@ -1069,20 +1069,15 @@ export function createRouter(init: RouterInit): Router { // Wire up subscribers to update loaderData as promises settle activeDeferreds.forEach((deferredData, routeId) => { - deferredData.subscribe((aborted, loaderDataKey, data) => { + deferredData.subscribe((aborted) => { if (aborted) { activeDeferreds.delete(routeId); return; } - // This will always be defined here, but TS doesn't know that - invariant(loaderDataKey, "Missing loaderDataKey in subscribe"); updateState({ loaderData: { ...state.loaderData, - [routeId]: { - ...state.loaderData[routeId], - [loaderDataKey]: data, - }, + [routeId]: deferredData.data, }, }); // Remove this instance if all promises have settled diff --git a/packages/router/utils.ts b/packages/router/utils.ts index 6dbed087e7..708b40bf10 100644 --- a/packages/router/utils.ts +++ b/packages/router/utils.ts @@ -852,37 +852,55 @@ export const json: JsonFunction = (data, init = {}) => { }; export class DeferredData { - private pendingKeys: Set = new Set(); + private pendingKeys: Set = new Set(); private cancelled: boolean = false; - private subscriber?: (aborted: boolean, key?: string, data?: any) => void = - undefined; - data: RouteData = {}; + private subscriber?: (aborted: boolean) => void = undefined; + data: RouteData | Array; constructor(data: Record) { - Object.entries(data).forEach(([key, value]) => { - // Store all data in our internal copy and track promise keys - this.data[key] = value; - if (value instanceof Promise) { - this.pendingKeys.add(key); - value.then( - (data) => this.onSettle(key, null, data), - (error) => this.onSettle(key, error) - ); - } - }); + // Store all data in our internal copy and track promise keys + if (Array.isArray(data)) { + this.data = [...data]; + data.forEach((value, index) => this.trackPromise(index, value)); + } else { + this.data = { ...data }; + Object.entries(data).forEach(([key, value]) => + this.trackPromise(key, value) + ); + } } - private onSettle(key: string, error: any, data?: any) { + private trackPromise(key: string | number, value: any) { + if (value instanceof Promise) { + this.pendingKeys.add(key); + value.then( + (data) => this.onSettle(key, null, data), + (error) => this.onSettle(key, error) + ); + } + } + + private onSettle(key: string | number, error: any, data?: any) { if (this.cancelled) { return; } this.pendingKeys.delete(key); let value = error ? new DeferredError(error) : data; - this.data[key] = value; - this.subscriber?.(false, key, value); + if (Array.isArray(this.data)) { + invariant(typeof key === "number", "expected key to be a number"); + let data = [...this.data]; + data[key] = value; + this.data = data; + } else { + this.data = { + ...this.data, + [key]: value, + }; + } + this.subscriber?.(false); } - subscribe(fn: (aborted: boolean, key?: string, data?: any) => void) { + subscribe(fn: (aborted: boolean) => void) { this.subscriber = fn; } From 4d94cfb10aef8859bbcf2a76fab8e040c417d750 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Mon, 18 Jul 2022 12:54:15 -0400 Subject: [PATCH 03/14] chore:" remove typings from Deferred and useDeferredData To be added back in once we figur eout typing for useLoaderData in 6.5 --- packages/react-router/lib/components.tsx | 29 ++++++++++++------------ packages/react-router/lib/hooks.tsx | 4 ++-- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/packages/react-router/lib/components.tsx b/packages/react-router/lib/components.tsx index 72a2619ac8..180ae52925 100644 --- a/packages/react-router/lib/components.tsx +++ b/packages/react-router/lib/components.tsx @@ -429,14 +429,14 @@ function DataRoutes({ return useRoutes(routes || createRoutesFromChildren(children), location); } -export interface DeferredResolveRenderFunction { - (data: Awaited): JSX.Element; +export interface DeferredResolveRenderFunction { + (data: Awaited): JSX.Element; } -export interface DeferredProps - extends Omit { - children: React.ReactNode | DeferredResolveRenderFunction; - value: Data; +export interface DeferredProps { + children: React.ReactNode | DeferredResolveRenderFunction; + value: any; + fallback: React.SuspenseProps["fallback"]; errorElement?: React.ReactNode; } @@ -444,19 +444,19 @@ export interface DeferredProps * Component to use for rendering lazily loaded data from returning deferred() * in a loader function */ -export function Deferred({ +export function Deferred({ children, value, fallback, errorElement, -}: DeferredProps) { +}: DeferredProps) { return ( {typeof children === "function" ? ( } + children={children as DeferredResolveRenderFunction} /> ) : ( children @@ -497,17 +497,16 @@ function DeferredWrapper({ children, errorElement }: DeferredWrapperProps) { return <>{children}; } -export interface ResolveDeferredProps { - children: DeferredResolveRenderFunction; +interface ResolveDeferredProps { + children: DeferredResolveRenderFunction; } /** * @private + * Indirection to leverage useDeferredData for a render-prop API on */ -export function ResolveDeferred({ - children, -}: ResolveDeferredProps) { - return children(useDeferredData()); +function ResolveDeferred({ children }: ResolveDeferredProps) { + return children(useDeferredData()); } /////////////////////////////////////////////////////////////////////////////// diff --git a/packages/react-router/lib/hooks.tsx b/packages/react-router/lib/hooks.tsx index 1bdd07487b..b11d155cf5 100644 --- a/packages/react-router/lib/hooks.tsx +++ b/packages/react-router/lib/hooks.tsx @@ -727,9 +727,9 @@ export function useRouteError() { /** * Returns the happy-path data from the nearest ancestor value */ -export function useDeferredData() { +export function useDeferredData() { let value = React.useContext(DeferredContext); - return value as Awaited; + return value; } const alreadyWarned: Record = {}; From d9cea85b6219057894536d7160964343b221b52c Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Mon, 18 Jul 2022 16:37:00 -0400 Subject: [PATCH 04/14] Handle deferred render errors at the boundary --- .../__tests__/DataMemoryRouter-test.tsx | 325 ++++++++++-------- packages/react-router/lib/components.tsx | 65 +++- packages/react-router/lib/hooks.tsx | 2 +- 3 files changed, 237 insertions(+), 155 deletions(-) diff --git a/packages/react-router/__tests__/DataMemoryRouter-test.tsx b/packages/react-router/__tests__/DataMemoryRouter-test.tsx index acb7c747b3..6558237d21 100644 --- a/packages/react-router/__tests__/DataMemoryRouter-test.tsx +++ b/packages/react-router/__tests__/DataMemoryRouter-test.tsx @@ -1890,7 +1890,11 @@ describe("", () => { }); describe("deferred", () => { - it("allows loaders to returned deferred data (child component)", async () => { + function setupDeferredTest( + hasRouteErrorElement = false, + hasDeferredErrorElement = false, + triggerRenderError = false + ) { let barDefer = defer(); let { container } = render( @@ -1900,6 +1904,7 @@ describe("", () => { path="bar" loader={() => barDefer.promise} element={} + errorElement={hasRouteErrorElement ? : null} /> @@ -1924,17 +1929,41 @@ describe("", () => { return ( <>

{data.critical}

- Loading...

}> + Loading...

} + errorElement={hasDeferredErrorElement ? : null} + >
); } + + function RouteError() { + let error = useRouteError(); + return

Route Error:{error.message}

; + } + function LazyData() { - let data = useDeferredData(); - return

{data}

; + let data = useDeferredData(); + return triggerRenderError ? ( +

{oops.i.did.it.again}

+ ) : ( +

{data}

+ ); } + function LazyError() { + let data = useRouteError(); + return

Handled Error:{data.message}

; + } + + return { container, barDefer }; + } + + it("allows loaders to returned deferred data (child component)", async () => { + let { barDefer, container } = setupDeferredTest(); fireEvent.click(screen.getByText("Link to Bar")); expect(getHtml(container)).toMatchInlineSnapshot(` "
@@ -2008,45 +2037,7 @@ describe("", () => { }); it("allows loaders to returned deferred data (render prop)", async () => { - let barDefer = defer(); - let { container } = render( - - }> - } /> - barDefer.promise} - element={} - /> - - - ); - - function Layout() { - let navigation = useNavigation(); - return ( -
- Link to Bar -

{navigation.state}

- -
- ); - } - - function Foo() { - return

Foo

; - } - function Bar() { - let data = useLoaderData(); - return ( - <> -

{data.critical}

- value={data.lazy} fallback={

Loading...

}> - {(data) =>

{data}

} - - - ); - } + let { barDefer, container } = setupDeferredTest(); fireEvent.click(screen.getByText("Link to Bar")); expect(getHtml(container)).toMatchInlineSnapshot(` @@ -2120,58 +2111,84 @@ describe("", () => { `); }); - it("sends errors to the provided errorElement", async () => { - let barDefer = defer(); - let { container } = render( - - }> - } /> - barDefer.promise} - element={} - /> - - - ); + it("sends data errors to the provided errorElement", async () => { + let { barDefer, container } = setupDeferredTest(true, true, false); - function Layout() { - let navigation = useNavigation(); - return ( + fireEvent.click(screen.getByText("Link to Bar")); + expect(getHtml(container)).toMatchInlineSnapshot(` + "
- Link to Bar -

{navigation.state}

- + + Link to Bar + +

+ loading +

+

+ Foo +

- ); - } +
" + `); - function Foo() { - return

Foo

; - } - function Bar() { - let data = useLoaderData(); - return ( - <> -

{data.critical}

- Loading...

} - errorElement={} + let barValueDfd = defer(); + barDefer.resolve( + deferred({ + critical: "CRITICAL", + lazy: barValueDfd.promise, + }) + ); + await waitFor(() => screen.getByText("idle")); + expect(getHtml(container)).toMatchInlineSnapshot(` + "" + `); + + barValueDfd.reject(new Error("Kaboom!")); + await waitFor(() => screen.getByText(/Kaboom!/)); + expect(getHtml(container)).toMatchInlineSnapshot(` + "
+
+ + Link to Bar + +

+ idle +

+

+ CRITICAL +

+

+ Handled Error: + Error: Kaboom! +

+
+
" + `); + }); + + it("sends unhandled data errors to the nearest route error boundary", async () => { + let { barDefer, container } = setupDeferredTest(true, false, false); fireEvent.click(screen.getByText("Link to Bar")); expect(getHtml(container)).toMatchInlineSnapshot(` @@ -2235,10 +2252,7 @@ describe("", () => { idle

- CRITICAL -

-

- Handled Error: + Route Error: Error: Kaboom!

@@ -2246,55 +2260,84 @@ describe("", () => { `); }); - it("sends unhandled errors to the nearest route error boundary", async () => { - let barDefer = defer(); - let { container } = render( - - }> - } /> - barDefer.promise} - element={} - errorElement={} - /> - - + it("sends render errors to the provided errorElement", async () => { + let { barDefer, container } = setupDeferredTest(true, true, true); + + fireEvent.click(screen.getByText("Link to Bar")); + expect(getHtml(container)).toMatchInlineSnapshot(` + "
+
+ + Link to Bar + +

+ loading +

+

+ Foo +

+
+
" + `); + + let barValueDfd = defer(); + barDefer.resolve( + deferred({ + critical: "CRITICAL", + lazy: barValueDfd.promise, + }) ); + await waitFor(() => screen.getByText("idle")); + expect(getHtml(container)).toMatchInlineSnapshot(` + "
+
+ + Link to Bar + +

+ idle +

+

+ CRITICAL +

+

+ Loading... +

+
+
" + `); - function Layout() { - let navigation = useNavigation(); - return ( + barValueDfd.resolve("LAZY"); + await waitFor(() => !screen.getByText(/Loading.../)); + expect(getHtml(container)).toMatchInlineSnapshot(` + "
- Link to Bar -

{navigation.state}

- + + Link to Bar + +

+ idle +

+

+ CRITICAL +

+

+ Handled Error: + oops is not defined +

- ); - } +
" + `); + }); - function Foo() { - return

Foo

; - } - function Bar() { - let data = useLoaderData(); - return ( - <> -

{data.critical}

- Loading...

}> - -
- - ); - } - function LazyData() { - let data = useDeferredData(); - return

{data}

; - } - function RouteError() { - let error = useRouteError(); - return

Route Error:{error.message}

; - } + it("sends unhandled render errors to the nearest route error boundary", async () => { + let { barDefer, container } = setupDeferredTest(true, false, true); fireEvent.click(screen.getByText("Link to Bar")); expect(getHtml(container)).toMatchInlineSnapshot(` @@ -2344,8 +2387,8 @@ describe("", () => { " `); - barValueDfd.reject(new Error("Kaboom!")); - await waitFor(() => screen.getByText(/Kaboom!/)); + barValueDfd.resolve("LAZY"); + await waitFor(() => !screen.getByText(/Loading.../)); expect(getHtml(container)).toMatchInlineSnapshot(` "
@@ -2359,7 +2402,7 @@ describe("", () => {

Route Error: - Error: Kaboom! + oops is not defined

" diff --git a/packages/react-router/lib/components.tsx b/packages/react-router/lib/components.tsx index 180ae52925..5eced6319b 100644 --- a/packages/react-router/lib/components.tsx +++ b/packages/react-router/lib/components.tsx @@ -451,9 +451,9 @@ export function Deferred({ errorElement, }: DeferredProps) { return ( - + - + {typeof children === "function" ? ( - + ); } +type DeferredErrorBoundaryProps = React.PropsWithChildren<{ + value: any; + errorElement?: React.ReactNode; +}>; + +type DeferredErrorBoundaryState = { + error: any; +}; + +class DeferredErrorBoundary extends React.Component< + DeferredErrorBoundaryProps, + DeferredErrorBoundaryState +> { + constructor(props: DeferredErrorBoundaryProps) { + super(props); + this.state = { error: null }; + } + + static getDerivedStateFromError(error: any) { + return { error }; + } + + componentDidCatch(error: any, errorInfo: any) { + console.error( + " caught the following error during render", + error, + errorInfo + ); + } + + render() { + let { children, errorElement, value } = this.props; + // Handle render errors from this.state, or data errors from context + let error = this.state.error || (isDeferredError(value) ? value : null); + + if (error) { + if (errorElement) { + return ( + + ); + } + // Throw to ancestor route-level error boundary + throw error; + } + return ; + } +} + interface DeferredWrapperProps { children: React.ReactNode; errorElement?: React.ReactNode; @@ -478,22 +526,13 @@ interface DeferredWrapperProps { * fallback, or rendering the children/errorElement once the promise resolves * or rejects */ -function DeferredWrapper({ children, errorElement }: DeferredWrapperProps) { +function DeferredWrapper({ children }: DeferredWrapperProps) { let value = React.useContext(DeferredContext); if (value instanceof Promise) { // throw to the suspense boundary throw value; } - if (isDeferredError(value)) { - if (errorElement) { - return <>{errorElement}; - } else { - // Throw to the nearest route-level error boundary - throw value; - } - } - return <>{children}; } diff --git a/packages/react-router/lib/hooks.tsx b/packages/react-router/lib/hooks.tsx index b11d155cf5..2cd151d1a7 100644 --- a/packages/react-router/lib/hooks.tsx +++ b/packages/react-router/lib/hooks.tsx @@ -704,7 +704,7 @@ export function useRouteError() { let deferredValue = React.useContext(DeferredContext); // Return deferred errors if we're inside a Deferred errorElement - if (deferredValue && isDeferredError(deferredValue)) { + if (deferredValue && deferredValue instanceof Error) { return deferredValue; } From 20e3add4fd2ebfd96b35b19929e55eeb02be1917 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Mon, 18 Jul 2022 16:52:39 -0400 Subject: [PATCH 05/14] internal renames --- packages/react-router/lib/components.tsx | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/packages/react-router/lib/components.tsx b/packages/react-router/lib/components.tsx index 5eced6319b..29f2528073 100644 --- a/packages/react-router/lib/components.tsx +++ b/packages/react-router/lib/components.tsx @@ -453,7 +453,7 @@ export function Deferred({ return ( - + {typeof children === "function" ? ( + ); @@ -515,18 +515,13 @@ class DeferredErrorBoundary extends React.Component< } } -interface DeferredWrapperProps { - children: React.ReactNode; - errorElement?: React.ReactNode; -} - /** * @private * Internal wrapper to handle re-throwing the promise to trigger the Suspense * fallback, or rendering the children/errorElement once the promise resolves * or rejects */ -function DeferredWrapper({ children }: DeferredWrapperProps) { +function DeferredThrower({ children }: { children: React.ReactNode }) { let value = React.useContext(DeferredContext); if (value instanceof Promise) { // throw to the suspense boundary @@ -536,15 +531,15 @@ function DeferredWrapper({ children }: DeferredWrapperProps) { return <>{children}; } -interface ResolveDeferredProps { - children: DeferredResolveRenderFunction; -} - /** * @private * Indirection to leverage useDeferredData for a render-prop API on */ -function ResolveDeferred({ children }: ResolveDeferredProps) { +function ResolveDeferred({ + children, +}: { + children: DeferredResolveRenderFunction; +}) { return children(useDeferredData()); } From d51e0ae2bc21c97a10371764c5c4ffc5415c3279 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Mon, 18 Jul 2022 17:16:59 -0400 Subject: [PATCH 06/14] do not handle fallback render errors in deferred error boundary --- .../__tests__/DataMemoryRouter-test.tsx | 85 ++++++++++++++++++- packages/react-router/lib/components.tsx | 47 ++++------ 2 files changed, 101 insertions(+), 31 deletions(-) diff --git a/packages/react-router/__tests__/DataMemoryRouter-test.tsx b/packages/react-router/__tests__/DataMemoryRouter-test.tsx index 6558237d21..15f1bbb62b 100644 --- a/packages/react-router/__tests__/DataMemoryRouter-test.tsx +++ b/packages/react-router/__tests__/DataMemoryRouter-test.tsx @@ -1893,7 +1893,8 @@ describe("", () => { function setupDeferredTest( hasRouteErrorElement = false, hasDeferredErrorElement = false, - triggerRenderError = false + triggerRenderError = false, + triggerFallbackError = false ) { let barDefer = defer(); let { container } = render( @@ -1931,7 +1932,7 @@ describe("", () => {

{data.critical}

Loading...

} + fallback={} errorElement={hasDeferredErrorElement ? : null} > @@ -1945,6 +1946,14 @@ describe("", () => { return

Route Error:{error.message}

; } + function LazyFallback() { + return triggerFallbackError ? ( +

{oops.i.did.it}

+ ) : ( +

Loading...

+ ); + } + function LazyData() { let data = useDeferredData(); return triggerRenderError ? ( @@ -2408,6 +2417,78 @@ describe("", () => { " `); }); + + it("does not handle fallback render errors in the Deferred errorElement", async () => { + let { barDefer, container } = setupDeferredTest(true, true, true, true); + + fireEvent.click(screen.getByText("Link to Bar")); + expect(getHtml(container)).toMatchInlineSnapshot(` + "
+
+ + Link to Bar + +

+ loading +

+

+ Foo +

+
+
" + `); + + let barValueDfd = defer(); + barDefer.resolve( + deferred({ + critical: "CRITICAL", + lazy: barValueDfd.promise, + }) + ); + await waitFor(() => screen.getByText("idle")); + expect(getHtml(container)).toMatchInlineSnapshot(` + "
+
+ + Link to Bar + +

+ idle +

+

+ Route Error: + oops is not defined +

+
+
" + `); + + // Resolving doesn't do anything + barValueDfd.resolve("LAZY"); + await new Promise((r) => setTimeout(r, 1)); + expect(getHtml(container)).toMatchInlineSnapshot(` + "
+
+ + Link to Bar + +

+ idle +

+

+ Route Error: + oops is not defined +

+
+
" + `); + }); }); }); diff --git a/packages/react-router/lib/components.tsx b/packages/react-router/lib/components.tsx index 29f2528073..aba362a1c7 100644 --- a/packages/react-router/lib/components.tsx +++ b/packages/react-router/lib/components.tsx @@ -451,19 +451,17 @@ export function Deferred({ errorElement, }: DeferredProps) { return ( - - - - {typeof children === "function" ? ( - - ) : ( - children - )} - - - + + + {typeof children === "function" ? ( + + ) : ( + children + )} + + ); } @@ -504,6 +502,7 @@ class DeferredErrorBoundary extends React.Component< if (error) { if (errorElement) { + // We have a Deferred errorElement, so provide our error and render it return ( ); @@ -511,24 +510,14 @@ class DeferredErrorBoundary extends React.Component< // Throw to ancestor route-level error boundary throw error; } - return ; - } -} -/** - * @private - * Internal wrapper to handle re-throwing the promise to trigger the Suspense - * fallback, or rendering the children/errorElement once the promise resolves - * or rejects - */ -function DeferredThrower({ children }: { children: React.ReactNode }) { - let value = React.useContext(DeferredContext); - if (value instanceof Promise) { - // throw to the suspense boundary - throw value; - } + if (value instanceof Promise) { + // Throw to the suspense boundary + throw value; + } - return <>{children}; + return ; + } } /** From f323d8763af15d46ddc6a93c6c3e03689fa2020d Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Tue, 19 Jul 2022 09:29:41 -0400 Subject: [PATCH 07/14] Remove Suspense from inside Deferred --- examples/data-router/src/App.tsx | 56 +++++++++++-------- .../__tests__/DataMemoryRouter-test.tsx | 15 ++--- packages/react-router/lib/components.tsx | 36 +++++------- 3 files changed, 56 insertions(+), 51 deletions(-) diff --git a/examples/data-router/src/App.tsx b/examples/data-router/src/App.tsx index 57d27e5cf2..f9b8f6ccaf 100644 --- a/examples/data-router/src/App.tsx +++ b/examples/data-router/src/App.tsx @@ -269,25 +269,35 @@ function DeferredPage() {

{data.critical1}

{data.critical2}

- should not see me!

}> - -
- loading 1...

}> - -
- loading 2...

}> - -
- loading 3...

}> - {(data) =>

{data}

} -
- loading (error)...

} - errorElement={} - > - -
+ + should not see me!

}> + + + +
+ + loading 1...

}> + + + +
+ + loading 2...

}> + + + +
+ + loading 3...

}> + {(data) =>

{data}

}
+
+ + loading (error)...

}> + }> + + +
+
); @@ -310,9 +320,11 @@ function DeferredChild() { return (

{data.critical}

- loading child...

}> - -
+ loading child...

}> + + + +