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

Add missing <Form state> prop #10630

Merged
merged 6 commits into from
Jun 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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/form-state-prop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"react-router-dom": patch
---

Add missing `<Form state>` prop to populate `history.state` on submission navigations
19 changes: 19 additions & 0 deletions docs/components/form.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,24 @@ See also:
- [`useActionData`][useactiondata]
- [`useSubmit`][usesubmit]

## `state`

The `state` property can be used to set a stateful value for the new location which is stored inside [history state][history-state]. This value can subsequently be accessed via `useLocation()`.

```tsx
<Form
method="post"
action="new-path"
state={{ some: "value" }}
/>
```

You can access this state value while on the "new-path" route:

```ts
let { state } = useLocation();
```

## `preventScrollReset`

If you are using [`<ScrollRestoration>`][scrollrestoration], this lets you prevent the scroll position from being reset to the top of the window when the form action redirects to a new location.
Expand Down Expand Up @@ -330,3 +348,4 @@ You can access those values from the `request.url`
[pickingarouter]: ../routers/picking-a-router
[scrollrestoration]: ./scroll-restoration
[link-preventscrollreset]: ./link#preventscrollreset
[history-state]: https://developer.mozilla.org/en-US/docs/Web/API/History/state
3 changes: 3 additions & 0 deletions docs/hooks/use-submit.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,4 +150,7 @@ submit(null, {
<Form action="/logout" method="post" />;
```

Because submissions are navigations, the options may also contain the other navigation related props from [`<Form>`][form] such as `replace`, `state`, `preventScrollReset`, `relative`, etc.

[pickingarouter]: ../routers/picking-a-router
[form]: ../components/form.md
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@
"none": "16.2 kB"
},
"packages/react-router-dom/dist/react-router-dom.production.min.js": {
"none": "12.7 kB"
"none": "12.8 kB"
},
"packages/react-router-dom/dist/umd/react-router-dom.production.min.js": {
"none": "18.7 kB"
Expand Down
40 changes: 40 additions & 0 deletions packages/react-router-dom/__tests__/data-browser-router-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1584,6 +1584,46 @@ function testDomRouter(
`);
});

it("supports <Form state>", async () => {
let testWindow = getWindow("/");
let router = createTestRouter(
[
{
path: "/",
Component() {
return (
<Form method="post" action="/action" state={{ key: "value" }}>
<button type="submit">Submit</button>
</Form>
);
},
},
{
path: "/action",
action: () => null,
Component() {
let state = useLocation().state;
return <p>{JSON.stringify(state)}</p>;
},
},
],
{ window: testWindow }
);
let { container } = render(<RouterProvider router={router} />);
expect(testWindow.history.state.usr).toBeUndefined();

fireEvent.click(screen.getByText("Submit"));
await waitFor(() => screen.getByText('{"key":"value"}'));
expect(getHtml(container)).toMatchInlineSnapshot(`
"<div>
<p>
{"key":"value"}
</p>
</div>"
`);
expect(testWindow.history.state.usr).toEqual({ key: "value" });
});

it("supports <Form reloadDocument={true}>", async () => {
let actionSpy = jest.fn();
let router = createTestRouter(
Expand Down
5 changes: 5 additions & 0 deletions packages/react-router-dom/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,11 @@ export interface SubmitOptions {
*/
replace?: boolean;

/**
* State object to add to the history stack entry for this navigation
*/
state?: any;

/**
* Determines whether the form action is relative to the route hierarchy or
* the pathname. Use this if you want to opt out of navigating the route
Expand Down
43 changes: 27 additions & 16 deletions packages/react-router-dom/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -716,7 +716,8 @@ if (__DEV__) {
NavLink.displayName = "NavLink";
}

export interface FormProps extends React.FormHTMLAttributes<HTMLFormElement> {
export interface FetcherFormProps
extends React.FormHTMLAttributes<HTMLFormElement> {
/**
* The HTTP verb to use when the form is submit. Supports "get", "post",
* "put", "delete", "patch".
Expand All @@ -737,18 +738,6 @@ export interface FormProps extends React.FormHTMLAttributes<HTMLFormElement> {
*/
action?: string;

/**
* Forces a full document navigation instead of a fetch.
*/
reloadDocument?: boolean;

/**
* Replaces the current entry in the browser history stack when the form
* navigates. Use this if you don't want the user to be able to click "back"
* to the page with the form on it.
*/
replace?: boolean;

/**
* Determines whether the form action is relative to the route hierarchy or
* the pathname. Use this if you want to opt out of navigating the route
Expand All @@ -769,6 +758,25 @@ export interface FormProps extends React.FormHTMLAttributes<HTMLFormElement> {
onSubmit?: React.FormEventHandler<HTMLFormElement>;
}

export interface FormProps extends FetcherFormProps {
/**
* Forces a full document navigation instead of a fetch.
*/
reloadDocument?: boolean;

/**
* Replaces the current entry in the browser history stack when the form
* navigates. Use this if you don't want the user to be able to click "back"
* to the page with the form on it.
*/
replace?: boolean;

/**
* State object to add to the history stack entry for this navigation
*/
state?: any;
}

/**
* A `@remix-run/router`-aware `<form>`. It behaves like a normal form except
* that the interaction with the server is with `fetch` instead of new document
Expand Down Expand Up @@ -803,6 +811,7 @@ const FormImpl = React.forwardRef<HTMLFormElement, FormImplProps>(
{
reloadDocument,
replace,
state,
method = defaultMethod,
action,
onSubmit,
Expand Down Expand Up @@ -831,6 +840,7 @@ const FormImpl = React.forwardRef<HTMLFormElement, FormImplProps>(
submit(submitter || event.currentTarget, {
method: submitMethod,
replace,
state,
relative,
preventScrollReset,
});
Expand Down Expand Up @@ -1048,8 +1058,8 @@ export interface SubmitFunction {
export interface FetcherSubmitFunction {
(
target: SubmitTarget,
// Fetchers cannot replace because they are not navigation events
options?: Omit<SubmitOptions, "replace">
// Fetchers cannot replace or set state because they are not navigation events
options?: Omit<SubmitOptions, "replace" | "state">
): void;
}

Expand Down Expand Up @@ -1087,6 +1097,7 @@ export function useSubmit(): SubmitFunction {
formMethod: options.method || (method as HTMLFormMethod),
formEncType: options.encType || (encType as FormEncType),
replace: options.replace,
state: options.state,
fromRouteId: currentRouteId,
});
},
Expand Down Expand Up @@ -1186,7 +1197,7 @@ export function useFormAction(
}

function createFetcherForm(fetcherKey: string, routeId: string) {
let FetcherForm = React.forwardRef<HTMLFormElement, FormProps>(
let FetcherForm = React.forwardRef<HTMLFormElement, FetcherFormProps>(
(props, ref) => {
let submit = useSubmitFetcher(fetcherKey, routeId);
return <FormImpl {...props} ref={ref} submit={submit} />;
Expand Down