Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updates for Remix on RR 6.4 #9664

Merged
merged 29 commits into from Dec 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
dc5dddf
Allow uppercase <Form> methods and fix submitted method override
brophdawg11 Dec 1, 2022
e2f0cca
Remove ?? operator and add lint rules
brophdawg11 Dec 1, 2022
3fce264
Add Error serialization
brophdawg11 Dec 1, 2022
cdbaa27
Mark loader-less routes with null loaderData during SSR
brophdawg11 Dec 1, 2022
e5d0803
Support fetch action redirects in useTransition
brophdawg11 Dec 2, 2022
c9e6851
Merge branch 'dev' into brophdawg11/remix-updates
brophdawg11 Dec 7, 2022
34ee733
Add _isRedirect state for useTransition back compat
brophdawg11 Dec 8, 2022
f188645
Updates for fetcher type and redirect replace logic
brophdawg11 Dec 8, 2022
e8d5340
Add tests for SSR null loader values on non-executed loaders
brophdawg11 Dec 9, 2022
99993d9
more tests for submission replace/push logic
brophdawg11 Dec 9, 2022
e3b31d0
fix test for null loader data during ssr
brophdawg11 Dec 9, 2022
470082b
SSr error serialization tests
brophdawg11 Dec 9, 2022
239bc7c
form tests
brophdawg11 Dec 9, 2022
13a0b4f
Merge branch 'dev' into brophdawg11/remix-updates
brophdawg11 Dec 12, 2022
ecbca64
bundle bump
brophdawg11 Dec 12, 2022
09209b6
Updates to ScrollRestoration for Remix
brophdawg11 Dec 12, 2022
a5e45d7
Export useBeforeUnload
brophdawg11 Dec 12, 2022
6c69af5
Avoid SSR layout effects
brophdawg11 Dec 12, 2022
9c3548f
Add changesets
brophdawg11 Dec 13, 2022
80a50b1
Merge branch 'dev' into brophdawg11/remix-updates
brophdawg11 Dec 13, 2022
6239420
Bundle bump
brophdawg11 Dec 14, 2022
4066857
Remove skip param
brophdawg11 Dec 15, 2022
8ab846b
Update chreck for json content type
brophdawg11 Dec 15, 2022
bb40a35
Revert "more tests for submission replace/push logic"
brophdawg11 Dec 15, 2022
71d7b2f
Revert "Updates for fetcher type and redirect replace logic"
brophdawg11 Dec 15, 2022
2da3b2c
Add _hasFetcherdoneAnything for Remix back-compat
brophdawg11 Dec 15, 2022
c3642e0
Remove changeset for redirect logic in new PR
brophdawg11 Dec 15, 2022
86699a1
Fix tests
brophdawg11 Dec 15, 2022
10b6c5b
Add changeset
brophdawg11 Dec 15, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/afraid-kiwis-grow.md
@@ -0,0 +1,5 @@
---
"react-router-dom": minor
---

Add `useBeforeUnload()` hook
5 changes: 5 additions & 0 deletions .changeset/bright-gorillas-pump.md
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
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
Expand Up @@ -7,6 +7,7 @@
"__DEV__": true
},
"rules": {
"strict": 0
"strict": 0,
"no-restricted-syntax": ["error", "LogicalExpression[operator='??']"]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See ?? removal below - added a lint rule to prevent that showing up again

}
}
204 changes: 200 additions & 4 deletions packages/react-router-dom/__tests__/data-browser-router-test.tsx
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
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
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 };
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lowercase incoming methods, since <Form method="POST"> is valid HTML

}