Skip to content

Commit fda0123

Browse files
authored
Allow useRevalidate to resolve a loader-driven error boundary UI (#10369)
1 parent 34779ab commit fda0123

File tree

5 files changed

+156
-148
lines changed

5 files changed

+156
-148
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"react-router": patch
3+
---
4+
5+
Allow `useRevalidator()` to resolve a loader-driven error boundary scenario

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,10 @@
108108
"none": "45.8 kB"
109109
},
110110
"packages/react-router/dist/react-router.production.min.js": {
111-
"none": "13.3 kB"
111+
"none": "13.5 kB"
112112
},
113113
"packages/react-router/dist/umd/react-router.production.min.js": {
114-
"none": "15.6 kB"
114+
"none": "15.8 kB"
115115
},
116116
"packages/react-router-dom/dist/react-router-dom.production.min.js": {
117117
"none": "12 kB"

packages/react-router/__tests__/data-memory-router-test.tsx

Lines changed: 135 additions & 142 deletions
Original file line numberDiff line numberDiff line change
@@ -909,7 +909,7 @@ describe("createMemoryRouter", () => {
909909
});
910910
});
911911

912-
it("reloads data using useRevalidate", async () => {
912+
it("reloads data using useRevalidator", async () => {
913913
let count = 1;
914914
let router = createMemoryRouter(
915915
createRoutesFromElements(
@@ -1747,37 +1747,11 @@ describe("createMemoryRouter", () => {
17471747
);
17481748
let { container } = render(<RouterProvider router={router} />);
17491749

1750-
expect(getHtml(container)).toMatchInlineSnapshot(`
1751-
"<div>
1752-
<h2>
1753-
Unexpected Application Error!
1754-
</h2>
1755-
<h3
1756-
style="font-style: italic;"
1757-
>
1758-
404 Not Found
1759-
</h3>
1760-
<p>
1761-
💿 Hey developer 👋
1762-
</p>
1763-
<p>
1764-
You can provide a way better UX than this when your app throws errors by providing your own
1765-
<code
1766-
style="padding: 2px 4px; background-color: rgba(200, 200, 200, 0.5);"
1767-
>
1768-
ErrorBoundary
1769-
</code>
1770-
or
1771-
1772-
<code
1773-
style="padding: 2px 4px; background-color: rgba(200, 200, 200, 0.5);"
1774-
>
1775-
errorElement
1776-
</code>
1777-
prop on your route.
1778-
</p>
1779-
</div>"
1780-
`);
1750+
let html = getHtml(container);
1751+
expect(html).toMatch("Unexpected Application Error!");
1752+
expect(html).toMatch("404 Not Found");
1753+
expect(html).toMatch("💿 Hey developer 👋");
1754+
expect(html).not.toMatch(/stack/i);
17811755
});
17821756

17831757
it("renders navigation errors with a default if no errorElements are provided", async () => {
@@ -1861,42 +1835,11 @@ describe("createMemoryRouter", () => {
18611835
error.stack = "FAKE STACK TRACE";
18621836
barDefer.reject(error);
18631837
await waitFor(() => screen.getByText("Kaboom!"));
1864-
expect(getHtml(container)).toMatchInlineSnapshot(`
1865-
"<div>
1866-
<h2>
1867-
Unexpected Application Error!
1868-
</h2>
1869-
<h3
1870-
style="font-style: italic;"
1871-
>
1872-
Kaboom!
1873-
</h3>
1874-
<pre
1875-
style="padding: 0.5rem; background-color: rgba(200, 200, 200, 0.5);"
1876-
>
1877-
FAKE STACK TRACE
1878-
</pre>
1879-
<p>
1880-
💿 Hey developer 👋
1881-
</p>
1882-
<p>
1883-
You can provide a way better UX than this when your app throws errors by providing your own
1884-
<code
1885-
style="padding: 2px 4px; background-color: rgba(200, 200, 200, 0.5);"
1886-
>
1887-
ErrorBoundary
1888-
</code>
1889-
or
1890-
1891-
<code
1892-
style="padding: 2px 4px; background-color: rgba(200, 200, 200, 0.5);"
1893-
>
1894-
errorElement
1895-
</code>
1896-
prop on your route.
1897-
</p>
1898-
</div>"
1899-
`);
1838+
let html = getHtml(container);
1839+
expect(html).toMatch("Unexpected Application Error!");
1840+
expect(html).toMatch("Kaboom!");
1841+
expect(html).toMatch("FAKE STACK TRACE");
1842+
expect(html).toMatch("💿 Hey developer 👋");
19001843
});
19011844

19021845
// This test ensures that when manual routes are used, we add hasErrorBoundary
@@ -2095,42 +2038,11 @@ describe("createMemoryRouter", () => {
20952038
throw error;
20962039
}
20972040

2098-
expect(getHtml(container)).toMatchInlineSnapshot(`
2099-
"<div>
2100-
<h2>
2101-
Unexpected Application Error!
2102-
</h2>
2103-
<h3
2104-
style="font-style: italic;"
2105-
>
2106-
Kaboom!
2107-
</h3>
2108-
<pre
2109-
style="padding: 0.5rem; background-color: rgba(200, 200, 200, 0.5);"
2110-
>
2111-
FAKE STACK TRACE
2112-
</pre>
2113-
<p>
2114-
💿 Hey developer 👋
2115-
</p>
2116-
<p>
2117-
You can provide a way better UX than this when your app throws errors by providing your own
2118-
<code
2119-
style="padding: 2px 4px; background-color: rgba(200, 200, 200, 0.5);"
2120-
>
2121-
ErrorBoundary
2122-
</code>
2123-
or
2124-
2125-
<code
2126-
style="padding: 2px 4px; background-color: rgba(200, 200, 200, 0.5);"
2127-
>
2128-
errorElement
2129-
</code>
2130-
prop on your route.
2131-
</p>
2132-
</div>"
2133-
`);
2041+
let html = getHtml(container);
2042+
expect(html).toMatch("Unexpected Application Error!");
2043+
expect(html).toMatch("Kaboom!");
2044+
expect(html).toMatch("FAKE STACK TRACE");
2045+
expect(html).toMatch("💿 Hey developer 👋");
21342046
});
21352047

21362048
it("does not handle render errors for non-data routers", async () => {
@@ -2280,44 +2192,11 @@ describe("createMemoryRouter", () => {
22802192

22812193
router.navigate("/child");
22822194
await waitFor(() => screen.getByText("Kaboom!"));
2283-
expect(getHtml(container)).toMatchInlineSnapshot(`
2284-
"<div>
2285-
<div>
2286-
<h2>
2287-
Unexpected Application Error!
2288-
</h2>
2289-
<h3
2290-
style="font-style: italic;"
2291-
>
2292-
Kaboom!
2293-
</h3>
2294-
<pre
2295-
style="padding: 0.5rem; background-color: rgba(200, 200, 200, 0.5);"
2296-
>
2297-
FAKE STACK TRACE
2298-
</pre>
2299-
<p>
2300-
💿 Hey developer 👋
2301-
</p>
2302-
<p>
2303-
You can provide a way better UX than this when your app throws errors by providing your own
2304-
<code
2305-
style="padding: 2px 4px; background-color: rgba(200, 200, 200, 0.5);"
2306-
>
2307-
ErrorBoundary
2308-
</code>
2309-
or
2310-
2311-
<code
2312-
style="padding: 2px 4px; background-color: rgba(200, 200, 200, 0.5);"
2313-
>
2314-
errorElement
2315-
</code>
2316-
prop on your route.
2317-
</p>
2318-
</div>
2319-
</div>"
2320-
`);
2195+
let html = getHtml(container);
2196+
expect(html).toMatch("Unexpected Application Error!");
2197+
expect(html).toMatch("Kaboom!");
2198+
expect(html).toMatch("FAKE STACK TRACE");
2199+
expect(html).toMatch("💿 Hey developer 👋");
23212200

23222201
router.navigate(-1);
23232202
await waitFor(() => {
@@ -2508,6 +2387,120 @@ describe("createMemoryRouter", () => {
25082387
);
25092388
errorSpy.mockRestore();
25102389
});
2390+
2391+
it("allows a successful useRevalidator to resolve the error boundary (loader + child boundary)", async () => {
2392+
let shouldFail = true;
2393+
let router = createMemoryRouter(
2394+
createRoutesFromElements(
2395+
<Route
2396+
path="/"
2397+
Component={() => (
2398+
<>
2399+
<MemoryNavigate to="child">/child</MemoryNavigate>
2400+
<Outlet />
2401+
</>
2402+
)}
2403+
>
2404+
<Route
2405+
path="child"
2406+
loader={() => {
2407+
if (shouldFail) {
2408+
shouldFail = false;
2409+
throw new Error("Broken");
2410+
} else {
2411+
return "Fixed";
2412+
}
2413+
}}
2414+
Component={() => <p>{("Child:" + useLoaderData()) as string}</p>}
2415+
ErrorBoundary={() => {
2416+
let { revalidate } = useRevalidator();
2417+
return (
2418+
<>
2419+
<p>{"Error:" + (useRouteError() as Error).message}</p>
2420+
<button onClick={() => revalidate()}>Try again</button>
2421+
</>
2422+
);
2423+
}}
2424+
/>
2425+
</Route>
2426+
)
2427+
);
2428+
2429+
let { container } = render(
2430+
<div>
2431+
<RouterProvider router={router} />
2432+
</div>
2433+
);
2434+
2435+
fireEvent.click(screen.getByText("/child"));
2436+
await waitFor(() => screen.getByText("Error:Broken"));
2437+
expect(getHtml(container)).toMatch("Error:Broken");
2438+
expect(router.state.errors).not.toBe(null);
2439+
2440+
fireEvent.click(screen.getByText("Try again"));
2441+
await waitFor(() => {
2442+
expect(queryByText(container, "Child:Fixed")).toBeInTheDocument();
2443+
});
2444+
expect(getHtml(container)).toMatch("Child:Fixed");
2445+
expect(router.state.errors).toBe(null);
2446+
});
2447+
2448+
it("allows a successful useRevalidator to resolve the error boundary (loader + parent boundary)", async () => {
2449+
let shouldFail = true;
2450+
let router = createMemoryRouter(
2451+
createRoutesFromElements(
2452+
<Route
2453+
path="/"
2454+
Component={() => (
2455+
<>
2456+
<MemoryNavigate to="child">/child</MemoryNavigate>
2457+
<Outlet />
2458+
</>
2459+
)}
2460+
ErrorBoundary={() => {
2461+
let { revalidate } = useRevalidator();
2462+
return (
2463+
<>
2464+
<p>{"Error:" + (useRouteError() as Error).message}</p>
2465+
<button onClick={() => revalidate()}>Try again</button>
2466+
</>
2467+
);
2468+
}}
2469+
>
2470+
<Route
2471+
path="child"
2472+
loader={() => {
2473+
if (shouldFail) {
2474+
shouldFail = false;
2475+
throw new Error("Broken");
2476+
} else {
2477+
return "Fixed";
2478+
}
2479+
}}
2480+
Component={() => <p>{("Child:" + useLoaderData()) as string}</p>}
2481+
/>
2482+
</Route>
2483+
)
2484+
);
2485+
2486+
let { container } = render(
2487+
<div>
2488+
<RouterProvider router={router} />
2489+
</div>
2490+
);
2491+
2492+
fireEvent.click(screen.getByText("/child"));
2493+
await waitFor(() => screen.getByText("Error:Broken"));
2494+
expect(getHtml(container)).toMatch("Error:Broken");
2495+
expect(router.state.errors).not.toBe(null);
2496+
2497+
fireEvent.click(screen.getByText("Try again"));
2498+
await waitFor(() => {
2499+
expect(queryByText(container, "Child:Fixed")).toBeInTheDocument();
2500+
});
2501+
expect(getHtml(container)).toMatch("Child:Fixed");
2502+
expect(router.state.errors).toBe(null);
2503+
});
25112504
});
25122505

25132506
describe("defer", () => {

0 commit comments

Comments
 (0)