Skip to content

Commit

Permalink
Add missing <Form state> prop (#10630)
Browse files Browse the repository at this point in the history
  • Loading branch information
brophdawg11 committed Jun 30, 2023
1 parent 499af9a commit bbc7715
Show file tree
Hide file tree
Showing 7 changed files with 100 additions and 17 deletions.
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

0 comments on commit bbc7715

Please sign in to comment.