Skip to content

Commit

Permalink
Updates for Remix on RR 6.4 (#9664)
Browse files Browse the repository at this point in the history
  • Loading branch information
brophdawg11 authored Dec 15, 2022
1 parent e54fbab commit cd7f118
Show file tree
Hide file tree
Showing 17 changed files with 629 additions and 88 deletions.
5 changes: 5 additions & 0 deletions .changeset/afraid-kiwis-grow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"react-router-dom": minor
---

Add `useBeforeUnload()` hook
5 changes: 5 additions & 0 deletions .changeset/bright-gorillas-pump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"react-router-dom": patch
---

Support uppercase `<Form method>` and `useSubmit` method values
5 changes: 5 additions & 0 deletions .changeset/empty-teachers-tie.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"react-router-dom": major
---

Proper hydration of `Error` objects from `StaticRouterProvider`
6 changes: 6 additions & 0 deletions .changeset/shiny-pants-decide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"react-router-dom": patch
"@remix-run/router": patch
---

Skip initial scroll restoration for SSR apps with hydrationData
5 changes: 5 additions & 0 deletions .changeset/small-dots-try.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"react-router-dom": patch
---

Fix `<button formmethod>` form submission overriddes
32 changes: 32 additions & 0 deletions docs/hooks/use-before-unload.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
title: useBeforeUnload
new: true
---

# `useBeforeUnload`

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.)

```tsx lines=[1,7-11]
import { useBeforeUnload } from "react-router-dom";

function SomeForm() {
const [state, setState] = React.useState(null);

// save it off before users navigate away
useBeforeUnload(
React.useCallback(() => {
localStorage.stuff = state;
}, [state])
);

// read it in when they return
React.useEffect(() => {
if (state === null && localStorage.stuff != null) {
setState(localStorage.stuff);
}
}, [state]);

return <>{/*... */}</>;
}
```
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@
},
"filesize": {
"packages/router/dist/router.umd.min.js": {
"none": "36.5 kB"
"none": "37 kB"
},
"packages/react-router/dist/react-router.production.min.js": {
"none": "12.5 kB"
Expand All @@ -116,7 +116,7 @@
"none": "14.5 kB"
},
"packages/react-router-dom/dist/react-router-dom.production.min.js": {
"none": "10.5 kB"
"none": "11 kB"
},
"packages/react-router-dom/dist/umd/react-router-dom.production.min.js": {
"none": "16.5 kB"
Expand Down
3 changes: 2 additions & 1 deletion packages/react-router-dom/.eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"__DEV__": true
},
"rules": {
"strict": 0
"strict": 0,
"no-restricted-syntax": ["error", "LogicalExpression[operator='??']"]
}
}
204 changes: 200 additions & 4 deletions packages/react-router-dom/__tests__/data-browser-router-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -287,14 +287,59 @@ function testDomRouter(

function Boundary() {
let error = useRouteError();
return isRouteErrorResponse(error) ? <h1>Yes!</h1> : <h2>No :(</h2>;
return isRouteErrorResponse(error) ? (
<pre>{JSON.stringify(error)}</pre>
) : (
<p>No :(</p>
);
}

expect(getHtml(container)).toMatchInlineSnapshot(`
"<div>
<h1>
Yes!
</h1>
<pre>
{\\"status\\":404,\\"statusText\\":\\"Not Found\\",\\"internal\\":false,\\"data\\":{\\"not\\":\\"found\\"}}
</pre>
</div>"
`);
});

it("deserializes Error instances from the window", async () => {
window.__staticRouterHydrationData = {
loaderData: {},
actionData: null,
errors: {
"0": {
message: "error message",
__type: "Error",
},
},
};
let { container } = render(
<TestDataRouter window={getWindow("/")}>
<Route path="/" element={<h1>Nope</h1>} errorElement={<Boundary />} />
</TestDataRouter>
);

function Boundary() {
let error = useRouteError();
return error instanceof Error ? (
<>
<pre>{error.toString()}</pre>
<pre>stack:{error.stack}</pre>
</>
) : (
<p>No :(</p>
);
}

expect(getHtml(container)).toMatchInlineSnapshot(`
"<div>
<pre>
Error: error message
</pre>
<pre>
stack:
</pre>
</div>"
`);
});
Expand Down Expand Up @@ -1523,6 +1568,157 @@ function testDomRouter(
`);
});

it("allows a button to override the <form method>", async () => {
let loaderDefer = createDeferred();

let { container } = render(
<TestDataRouter window={getWindow("/")} hydrationData={{}}>
<Route
path="/"
action={async ({ request }) => {
throw new Error("Should not hit this");
}}
loader={() => loaderDefer.promise}
element={<Home />}
/>
</TestDataRouter>
);

function Home() {
let data = useLoaderData();
let navigation = useNavigation();
return (
<div>
<Form
method="post"
onSubmit={(e) => {
// jsdom doesn't handle submitter so we add it here
// See https://github.com/jsdom/jsdom/issues/3117
// @ts-expect-error
e.nativeEvent.submitter =
e.currentTarget.querySelector("button");
}}
>
<input name="test" value="value" />
<button type="submit" formMethod="get">
Submit Form
</button>
</Form>
<div id="output">
<p>{navigation.state}</p>
<p>{data}</p>
</div>
<Outlet />
</div>
);
}

expect(getHtml(container.querySelector("#output")))
.toMatchInlineSnapshot(`
"<div
id=\\"output\\"
>
<p>
idle
</p>
<p />
</div>"
`);

fireEvent.click(screen.getByText("Submit Form"));
await waitFor(() => screen.getByText("loading"));
expect(getHtml(container.querySelector("#output")))
.toMatchInlineSnapshot(`
"<div
id=\\"output\\"
>
<p>
loading
</p>
<p />
</div>"
`);

loaderDefer.resolve("Loader Data");
await waitFor(() => screen.getByText("idle"));
expect(getHtml(container.querySelector("#output")))
.toMatchInlineSnapshot(`
"<div
id=\\"output\\"
>
<p>
idle
</p>
<p>
Loader Data
</p>
</div>"
`);
});

it("supports uppercase form method attributes", async () => {
let loaderDefer = createDeferred();
let actionDefer = createDeferred();

let { container } = render(
<TestDataRouter window={getWindow("/")} hydrationData={{}}>
<Route
path="/"
action={async ({ request }) => {
let resolvedValue = await actionDefer.promise;
let formData = await request.formData();
return `${resolvedValue}:${formData.get("test")}`;
}}
loader={() => loaderDefer.promise}
element={<Home />}
/>
</TestDataRouter>
);

function Home() {
let data = useLoaderData();
let actionData = useActionData();
let navigation = useNavigation();
return (
<div>
<Form method="POST">
<input name="test" value="value" />
<button type="submit">Submit Form</button>
</Form>
<div id="output">
<p>{navigation.state}</p>
<p>{data}</p>
<p>{actionData}</p>
</div>
<Outlet />
</div>
);
}

fireEvent.click(screen.getByText("Submit Form"));
await waitFor(() => screen.getByText("submitting"));
actionDefer.resolve("Action Data");
await waitFor(() => screen.getByText("loading"));
loaderDefer.resolve("Loader Data");
await waitFor(() => screen.getByText("idle"));
expect(getHtml(container.querySelector("#output")))
.toMatchInlineSnapshot(`
"<div
id=\\"output\\"
>
<p>
idle
</p>
<p>
Loader Data
</p>
<p>
Action Data:value
</p>
</div>"
`);
});

describe("<Form action>", () => {
function NoActionComponent() {
return (
Expand Down
49 changes: 48 additions & 1 deletion packages/react-router-dom/__tests__/data-static-router-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,50 @@ describe("A <StaticRouterProvider>", () => {
);
});

it("serializes Error instances", async () => {
let routes = [
{
path: "/",
loader: () => {
throw new Error("oh no");
},
},
];
let { query } = createStaticHandler(routes);

let context = (await query(
new Request("http://localhost/", {
signal: new AbortController().signal,
})
)) as StaticHandlerContext;

let html = ReactDOMServer.renderToStaticMarkup(
<React.StrictMode>
<StaticRouterProvider
router={createStaticRouter(routes, context)}
context={context}
/>
</React.StrictMode>
);

// stack is stripped by default from SSR errors
let expectedJsonString = JSON.stringify(
JSON.stringify({
loaderData: {},
actionData: null,
errors: {
"0": {
message: "oh no",
__type: "Error",
},
},
})
);
expect(html).toMatch(
`<script>window.__staticRouterHydrationData = JSON.parse(${expectedJsonString});</script>`
);
});

it("supports a nonce prop", async () => {
let routes = [
{
Expand Down Expand Up @@ -355,7 +399,10 @@ describe("A <StaticRouterProvider>", () => {

let expectedJsonString = JSON.stringify(
JSON.stringify({
loaderData: {},
loaderData: {
0: null,
"0-0": null,
},
actionData: null,
errors: null,
})
Expand Down
2 changes: 1 addition & 1 deletion packages/react-router-dom/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,5 +245,5 @@ export function getFormSubmissionInfo(
let { protocol, host } = window.location;
let url = new URL(action, `${protocol}//${host}`);

return { url, method, encType, formData };
return { url, method: method.toLowerCase(), encType, formData };
}
Loading

0 comments on commit cd7f118

Please sign in to comment.