Skip to content

Commit cd7f118

Browse files
authored
Updates for Remix on RR 6.4 (#9664)
1 parent e54fbab commit cd7f118

File tree

17 files changed

+629
-88
lines changed

17 files changed

+629
-88
lines changed

.changeset/afraid-kiwis-grow.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"react-router-dom": minor
3+
---
4+
5+
Add `useBeforeUnload()` hook

.changeset/bright-gorillas-pump.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"react-router-dom": patch
3+
---
4+
5+
Support uppercase `<Form method>` and `useSubmit` method values

.changeset/empty-teachers-tie.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"react-router-dom": major
3+
---
4+
5+
Proper hydration of `Error` objects from `StaticRouterProvider`

.changeset/shiny-pants-decide.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"react-router-dom": patch
3+
"@remix-run/router": patch
4+
---
5+
6+
Skip initial scroll restoration for SSR apps with hydrationData

.changeset/small-dots-try.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"react-router-dom": patch
3+
---
4+
5+
Fix `<button formmethod>` form submission overriddes

docs/hooks/use-before-unload.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
title: useBeforeUnload
3+
new: true
4+
---
5+
6+
# `useBeforeUnload`
7+
8+
This hook is just a helper around `window.onbeforeunload`. It can be useful to save important application state on the page (to something like the browser's local storage), before the user navigates away from your page. That way if they come back you can restore any stateful information (restore form input values, etc.)
9+
10+
```tsx lines=[1,7-11]
11+
import { useBeforeUnload } from "react-router-dom";
12+
13+
function SomeForm() {
14+
const [state, setState] = React.useState(null);
15+
16+
// save it off before users navigate away
17+
useBeforeUnload(
18+
React.useCallback(() => {
19+
localStorage.stuff = state;
20+
}, [state])
21+
);
22+
23+
// read it in when they return
24+
React.useEffect(() => {
25+
if (state === null && localStorage.stuff != null) {
26+
setState(localStorage.stuff);
27+
}
28+
}, [state]);
29+
30+
return <>{/*... */}</>;
31+
}
32+
```

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@
107107
},
108108
"filesize": {
109109
"packages/router/dist/router.umd.min.js": {
110-
"none": "36.5 kB"
110+
"none": "37 kB"
111111
},
112112
"packages/react-router/dist/react-router.production.min.js": {
113113
"none": "12.5 kB"
@@ -116,7 +116,7 @@
116116
"none": "14.5 kB"
117117
},
118118
"packages/react-router-dom/dist/react-router-dom.production.min.js": {
119-
"none": "10.5 kB"
119+
"none": "11 kB"
120120
},
121121
"packages/react-router-dom/dist/umd/react-router-dom.production.min.js": {
122122
"none": "16.5 kB"

packages/react-router-dom/.eslintrc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"__DEV__": true
88
},
99
"rules": {
10-
"strict": 0
10+
"strict": 0,
11+
"no-restricted-syntax": ["error", "LogicalExpression[operator='??']"]
1112
}
1213
}

packages/react-router-dom/__tests__/data-browser-router-test.tsx

Lines changed: 200 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -287,14 +287,59 @@ function testDomRouter(
287287

288288
function Boundary() {
289289
let error = useRouteError();
290-
return isRouteErrorResponse(error) ? <h1>Yes!</h1> : <h2>No :(</h2>;
290+
return isRouteErrorResponse(error) ? (
291+
<pre>{JSON.stringify(error)}</pre>
292+
) : (
293+
<p>No :(</p>
294+
);
291295
}
292296

293297
expect(getHtml(container)).toMatchInlineSnapshot(`
294298
"<div>
295-
<h1>
296-
Yes!
297-
</h1>
299+
<pre>
300+
{\\"status\\":404,\\"statusText\\":\\"Not Found\\",\\"internal\\":false,\\"data\\":{\\"not\\":\\"found\\"}}
301+
</pre>
302+
</div>"
303+
`);
304+
});
305+
306+
it("deserializes Error instances from the window", async () => {
307+
window.__staticRouterHydrationData = {
308+
loaderData: {},
309+
actionData: null,
310+
errors: {
311+
"0": {
312+
message: "error message",
313+
__type: "Error",
314+
},
315+
},
316+
};
317+
let { container } = render(
318+
<TestDataRouter window={getWindow("/")}>
319+
<Route path="/" element={<h1>Nope</h1>} errorElement={<Boundary />} />
320+
</TestDataRouter>
321+
);
322+
323+
function Boundary() {
324+
let error = useRouteError();
325+
return error instanceof Error ? (
326+
<>
327+
<pre>{error.toString()}</pre>
328+
<pre>stack:{error.stack}</pre>
329+
</>
330+
) : (
331+
<p>No :(</p>
332+
);
333+
}
334+
335+
expect(getHtml(container)).toMatchInlineSnapshot(`
336+
"<div>
337+
<pre>
338+
Error: error message
339+
</pre>
340+
<pre>
341+
stack:
342+
</pre>
298343
</div>"
299344
`);
300345
});
@@ -1523,6 +1568,157 @@ function testDomRouter(
15231568
`);
15241569
});
15251570

1571+
it("allows a button to override the <form method>", async () => {
1572+
let loaderDefer = createDeferred();
1573+
1574+
let { container } = render(
1575+
<TestDataRouter window={getWindow("/")} hydrationData={{}}>
1576+
<Route
1577+
path="/"
1578+
action={async ({ request }) => {
1579+
throw new Error("Should not hit this");
1580+
}}
1581+
loader={() => loaderDefer.promise}
1582+
element={<Home />}
1583+
/>
1584+
</TestDataRouter>
1585+
);
1586+
1587+
function Home() {
1588+
let data = useLoaderData();
1589+
let navigation = useNavigation();
1590+
return (
1591+
<div>
1592+
<Form
1593+
method="post"
1594+
onSubmit={(e) => {
1595+
// jsdom doesn't handle submitter so we add it here
1596+
// See https://github.com/jsdom/jsdom/issues/3117
1597+
// @ts-expect-error
1598+
e.nativeEvent.submitter =
1599+
e.currentTarget.querySelector("button");
1600+
}}
1601+
>
1602+
<input name="test" value="value" />
1603+
<button type="submit" formMethod="get">
1604+
Submit Form
1605+
</button>
1606+
</Form>
1607+
<div id="output">
1608+
<p>{navigation.state}</p>
1609+
<p>{data}</p>
1610+
</div>
1611+
<Outlet />
1612+
</div>
1613+
);
1614+
}
1615+
1616+
expect(getHtml(container.querySelector("#output")))
1617+
.toMatchInlineSnapshot(`
1618+
"<div
1619+
id=\\"output\\"
1620+
>
1621+
<p>
1622+
idle
1623+
</p>
1624+
<p />
1625+
</div>"
1626+
`);
1627+
1628+
fireEvent.click(screen.getByText("Submit Form"));
1629+
await waitFor(() => screen.getByText("loading"));
1630+
expect(getHtml(container.querySelector("#output")))
1631+
.toMatchInlineSnapshot(`
1632+
"<div
1633+
id=\\"output\\"
1634+
>
1635+
<p>
1636+
loading
1637+
</p>
1638+
<p />
1639+
</div>"
1640+
`);
1641+
1642+
loaderDefer.resolve("Loader Data");
1643+
await waitFor(() => screen.getByText("idle"));
1644+
expect(getHtml(container.querySelector("#output")))
1645+
.toMatchInlineSnapshot(`
1646+
"<div
1647+
id=\\"output\\"
1648+
>
1649+
<p>
1650+
idle
1651+
</p>
1652+
<p>
1653+
Loader Data
1654+
</p>
1655+
</div>"
1656+
`);
1657+
});
1658+
1659+
it("supports uppercase form method attributes", async () => {
1660+
let loaderDefer = createDeferred();
1661+
let actionDefer = createDeferred();
1662+
1663+
let { container } = render(
1664+
<TestDataRouter window={getWindow("/")} hydrationData={{}}>
1665+
<Route
1666+
path="/"
1667+
action={async ({ request }) => {
1668+
let resolvedValue = await actionDefer.promise;
1669+
let formData = await request.formData();
1670+
return `${resolvedValue}:${formData.get("test")}`;
1671+
}}
1672+
loader={() => loaderDefer.promise}
1673+
element={<Home />}
1674+
/>
1675+
</TestDataRouter>
1676+
);
1677+
1678+
function Home() {
1679+
let data = useLoaderData();
1680+
let actionData = useActionData();
1681+
let navigation = useNavigation();
1682+
return (
1683+
<div>
1684+
<Form method="POST">
1685+
<input name="test" value="value" />
1686+
<button type="submit">Submit Form</button>
1687+
</Form>
1688+
<div id="output">
1689+
<p>{navigation.state}</p>
1690+
<p>{data}</p>
1691+
<p>{actionData}</p>
1692+
</div>
1693+
<Outlet />
1694+
</div>
1695+
);
1696+
}
1697+
1698+
fireEvent.click(screen.getByText("Submit Form"));
1699+
await waitFor(() => screen.getByText("submitting"));
1700+
actionDefer.resolve("Action Data");
1701+
await waitFor(() => screen.getByText("loading"));
1702+
loaderDefer.resolve("Loader Data");
1703+
await waitFor(() => screen.getByText("idle"));
1704+
expect(getHtml(container.querySelector("#output")))
1705+
.toMatchInlineSnapshot(`
1706+
"<div
1707+
id=\\"output\\"
1708+
>
1709+
<p>
1710+
idle
1711+
</p>
1712+
<p>
1713+
Loader Data
1714+
</p>
1715+
<p>
1716+
Action Data:value
1717+
</p>
1718+
</div>"
1719+
`);
1720+
});
1721+
15261722
describe("<Form action>", () => {
15271723
function NoActionComponent() {
15281724
return (

packages/react-router-dom/__tests__/data-static-router-test.tsx

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,50 @@ describe("A <StaticRouterProvider>", () => {
321321
);
322322
});
323323

324+
it("serializes Error instances", async () => {
325+
let routes = [
326+
{
327+
path: "/",
328+
loader: () => {
329+
throw new Error("oh no");
330+
},
331+
},
332+
];
333+
let { query } = createStaticHandler(routes);
334+
335+
let context = (await query(
336+
new Request("http://localhost/", {
337+
signal: new AbortController().signal,
338+
})
339+
)) as StaticHandlerContext;
340+
341+
let html = ReactDOMServer.renderToStaticMarkup(
342+
<React.StrictMode>
343+
<StaticRouterProvider
344+
router={createStaticRouter(routes, context)}
345+
context={context}
346+
/>
347+
</React.StrictMode>
348+
);
349+
350+
// stack is stripped by default from SSR errors
351+
let expectedJsonString = JSON.stringify(
352+
JSON.stringify({
353+
loaderData: {},
354+
actionData: null,
355+
errors: {
356+
"0": {
357+
message: "oh no",
358+
__type: "Error",
359+
},
360+
},
361+
})
362+
);
363+
expect(html).toMatch(
364+
`<script>window.__staticRouterHydrationData = JSON.parse(${expectedJsonString});</script>`
365+
);
366+
});
367+
324368
it("supports a nonce prop", async () => {
325369
let routes = [
326370
{
@@ -355,7 +399,10 @@ describe("A <StaticRouterProvider>", () => {
355399

356400
let expectedJsonString = JSON.stringify(
357401
JSON.stringify({
358-
loaderData: {},
402+
loaderData: {
403+
0: null,
404+
"0-0": null,
405+
},
359406
actionData: null,
360407
errors: null,
361408
})

0 commit comments

Comments
 (0)