Skip to content

Commit 7f2b363

Browse files
committed
Add support for Await
1 parent 39e0009 commit 7f2b363

File tree

3 files changed

+217
-16
lines changed

3 files changed

+217
-16
lines changed

packages/react-router/__tests__/dom/handle-error-test.tsx

Lines changed: 179 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,22 @@
1-
import { act, fireEvent, render, waitFor } from "@testing-library/react";
1+
import {
2+
act,
3+
fireEvent,
4+
render,
5+
screen,
6+
waitFor,
7+
} from "@testing-library/react";
28
import * as React from "react";
39

4-
import { RouterProvider, createMemoryRouter, useFetcher } from "../../index";
10+
import {
11+
Await,
12+
RouterProvider,
13+
createMemoryRouter,
14+
useFetcher,
15+
useLoaderData,
16+
} from "../../index";
517

6-
import getHtml from "../utils/getHtml";
718
import { createFormData } from "../router/utils/utils";
19+
import getHtml from "../utils/getHtml";
820

921
describe(`handleError`, () => {
1022
let consoleError: jest.SpyInstance;
@@ -199,6 +211,168 @@ describe(`handleError`, () => {
199211
expect(getHtml(container)).toContain("Error");
200212
});
201213

214+
it("handles deferred data rejections from <Await>", async () => {
215+
let spy = jest.fn();
216+
let router = createMemoryRouter([
217+
{
218+
path: "/",
219+
Component: () => <h1>Home</h1>,
220+
},
221+
{
222+
path: "/page",
223+
loader() {
224+
return {
225+
promise: new Promise((_, r) =>
226+
setTimeout(() => r(new Error("await error!")), 1),
227+
),
228+
};
229+
},
230+
Component() {
231+
let data = useLoaderData();
232+
return (
233+
<Await resolve={data.promise} errorElement={<h1>Await Error</h1>}>
234+
{() => <p>Should not see me</p>}
235+
</Await>
236+
);
237+
},
238+
},
239+
]);
240+
241+
let { container } = render(
242+
<RouterProvider router={router} unstable_handleError={spy} />,
243+
);
244+
245+
await act(() => router.navigate("/page"));
246+
await waitFor(() => screen.getByText("Await Error"));
247+
248+
expect(spy.mock.calls).toEqual([
249+
[
250+
new Error("await error!"),
251+
{
252+
location: expect.objectContaining({ pathname: "/page" }),
253+
},
254+
],
255+
]);
256+
expect(getHtml(container)).toContain("Await Error");
257+
});
258+
259+
it("handles render errors from Await components", async () => {
260+
let spy = jest.fn();
261+
let router = createMemoryRouter([
262+
{
263+
path: "/",
264+
Component: () => <h1>Home</h1>,
265+
},
266+
{
267+
path: "/page",
268+
loader() {
269+
return {
270+
promise: new Promise((r) => setTimeout(() => r("data"), 10)),
271+
};
272+
},
273+
Component() {
274+
let data = useLoaderData();
275+
return (
276+
<React.Suspense fallback={<p>Loading...</p>}>
277+
<Await resolve={data.promise} errorElement={<h1>Await Error</h1>}>
278+
<RenderAwait />
279+
</Await>
280+
</React.Suspense>
281+
);
282+
},
283+
},
284+
]);
285+
286+
function RenderAwait() {
287+
throw new Error("await error!");
288+
// eslint-disable-next-line no-unreachable
289+
return <p>should not see me</p>;
290+
}
291+
292+
let { container } = render(
293+
<RouterProvider router={router} unstable_handleError={spy} />,
294+
);
295+
296+
await act(() => router.navigate("/page"));
297+
await waitFor(() => screen.getByText("Await Error"));
298+
299+
expect(spy.mock.calls).toEqual([
300+
[
301+
new Error("await error!"),
302+
{
303+
location: expect.objectContaining({ pathname: "/page" }),
304+
errorInfo: expect.objectContaining({
305+
componentStack: expect.any(String),
306+
}),
307+
},
308+
],
309+
]);
310+
expect(getHtml(container)).toContain("Await Error");
311+
});
312+
313+
it("handles render errors from Await errorElement", async () => {
314+
let spy = jest.fn();
315+
let router = createMemoryRouter([
316+
{
317+
path: "/",
318+
Component: () => <h1>Home</h1>,
319+
},
320+
{
321+
path: "/page",
322+
loader() {
323+
return {
324+
promise: new Promise((_, r) =>
325+
setTimeout(() => r(new Error("await error!")), 10),
326+
),
327+
};
328+
},
329+
Component() {
330+
let data = useLoaderData();
331+
return (
332+
<React.Suspense fallback={<p>Loading...</p>}>
333+
<Await resolve={data.promise} errorElement={<RenderError />}>
334+
{() => <p>Should not see me</p>}
335+
</Await>
336+
</React.Suspense>
337+
);
338+
},
339+
ErrorBoundary: () => <h1>Route Error</h1>,
340+
},
341+
]);
342+
343+
function RenderError() {
344+
throw new Error("errorElement error!");
345+
// eslint-disable-next-line no-unreachable
346+
return <p>should not see me</p>;
347+
}
348+
349+
let { container } = render(
350+
<RouterProvider router={router} unstable_handleError={spy} />,
351+
);
352+
353+
await act(() => router.navigate("/page"));
354+
await waitFor(() => screen.getByText("Route Error"));
355+
356+
expect(spy.mock.calls).toEqual([
357+
[
358+
new Error("await error!"),
359+
{
360+
location: expect.objectContaining({ pathname: "/page" }),
361+
},
362+
],
363+
[
364+
new Error("errorElement error!"),
365+
{
366+
location: expect.objectContaining({ pathname: "/page" }),
367+
errorInfo: expect.objectContaining({
368+
componentStack: expect.any(String),
369+
}),
370+
},
371+
],
372+
]);
373+
expect(getHtml(container)).toContain("Route Error");
374+
});
375+
202376
it("doesn't double report on state updates during an error boundary from a data error", async () => {
203377
let spy = jest.fn();
204378
let router = createMemoryRouter([
@@ -250,7 +424,7 @@ describe(`handleError`, () => {
250424

251425
// Doesn't re-call on a fetcher update from a rendered error boundary
252426
await fireEvent.click(container.querySelector("button")!);
253-
await waitFor(() => (getHtml(container) as string).includes("FETCH"));
427+
await waitFor(() => screen.getByText("FETCH"));
254428
expect(spy.mock.calls.length).toBe(1);
255429
});
256430

@@ -306,7 +480,7 @@ describe(`handleError`, () => {
306480

307481
// Doesn't re-call on a fetcher update from a rendered error boundary
308482
await fireEvent.click(container.querySelector("button")!);
309-
await waitFor(() => (getHtml(container) as string).includes("FETCH"));
483+
await waitFor(() => screen.getByText("FETCH"));
310484
expect(spy.mock.calls.length).toBe(1);
311485
});
312486
});

packages/react-router/lib/components.tsx

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -565,8 +565,9 @@ export function RouterProvider({
565565
navigator,
566566
static: false,
567567
basename,
568+
unstable_handleError,
568569
}),
569-
[router, navigator, basename],
570+
[router, navigator, basename, unstable_handleError],
570571
);
571572

572573
// The fragment and {null} here are important! We need them to keep React 18's
@@ -1400,8 +1401,17 @@ export function Await<Resolve>({
14001401
errorElement,
14011402
resolve,
14021403
}: AwaitProps<Resolve>) {
1404+
let dataRouterContext = React.useContext(DataRouterContext);
1405+
// Use this instead of useLocation() so that Await can still be used standalone
1406+
// and not inside of a <Router>
1407+
let dataRouterStateContext = React.useContext(DataRouterStateContext);
14031408
return (
1404-
<AwaitErrorBoundary resolve={resolve} errorElement={errorElement}>
1409+
<AwaitErrorBoundary
1410+
resolve={resolve}
1411+
errorElement={errorElement}
1412+
location={dataRouterStateContext?.location}
1413+
unstable_handleError={dataRouterContext?.unstable_handleError}
1414+
>
14051415
<ResolveAwait>{children}</ResolveAwait>
14061416
</AwaitErrorBoundary>
14071417
);
@@ -1410,6 +1420,8 @@ export function Await<Resolve>({
14101420
type AwaitErrorBoundaryProps = React.PropsWithChildren<{
14111421
errorElement?: React.ReactNode;
14121422
resolve: TrackedPromise | any;
1423+
location?: Location;
1424+
unstable_handleError?: unstable_HandleErrorFunction;
14131425
}>;
14141426

14151427
type AwaitErrorBoundaryState = {
@@ -1435,12 +1447,20 @@ class AwaitErrorBoundary extends React.Component<
14351447
return { error };
14361448
}
14371449

1438-
componentDidCatch(error: any, errorInfo: any) {
1439-
console.error(
1440-
"<Await> caught the following error during render",
1441-
error,
1442-
errorInfo,
1443-
);
1450+
componentDidCatch(error: any, errorInfo: React.ErrorInfo) {
1451+
if (this.props.unstable_handleError && this.props.location) {
1452+
// Log render errors
1453+
this.props.unstable_handleError(error, {
1454+
location: this.props.location,
1455+
errorInfo,
1456+
});
1457+
} else {
1458+
console.error(
1459+
"<Await> caught the following error during render",
1460+
error,
1461+
errorInfo,
1462+
);
1463+
}
14441464
}
14451465

14461466
render() {
@@ -1478,8 +1498,13 @@ class AwaitErrorBoundary extends React.Component<
14781498
promise = resolve.then(
14791499
(data: any) =>
14801500
Object.defineProperty(resolve, "_data", { get: () => data }),
1481-
(error: any) =>
1482-
Object.defineProperty(resolve, "_error", { get: () => error }),
1501+
(error: any) => {
1502+
// Log promise rejections
1503+
this.props.unstable_handleError?.(error, {
1504+
location: this.props.location,
1505+
});
1506+
Object.defineProperty(resolve, "_error", { get: () => error });
1507+
},
14831508
);
14841509
}
14851510

packages/react-router/lib/context.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import * as React from "react";
2+
import type { unstable_HandleErrorFunction } from "./components";
23
import type {
34
History,
4-
Action as NavigationType,
55
Location,
6+
Action as NavigationType,
67
To,
78
} from "./router/history";
89
import type {
@@ -90,6 +91,7 @@ export interface DataRouterContextObject
9091
extends Omit<NavigationContextObject, "future"> {
9192
router: Router;
9293
staticContext?: StaticHandlerContext;
94+
unstable_handleError?: unstable_HandleErrorFunction;
9395
}
9496

9597
export const DataRouterContext =

0 commit comments

Comments
 (0)