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 better control over submission serialization #10342

Merged
merged 20 commits into from
Apr 21, 2023
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .changeset/raw-payload-submission-router.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
"@remix-run/router": minor
---

Add support for a new `payload` parameter for `router.navigate`/`router.fetch` submissions. This allows you to submit data to an `action` without requiring serialization into a `FormData` instance. This `payload` value will be passed unaltered to your `action` function.

```js
router.navigate("/", { payload: { key: "value" } });

function action({ request, payload }) {
// payload => { key: 'value' }
// request.body => null
}
```

You may also opt-into serialization of this `payload` into your `request` using the `formEncType` parameter:

- `formEncType: "application/x-ww-form-urlencoded"` => serializes into `request.formData()`
- `formEncType: "application/json"` => serializes into `request.json()`
- `formEncType: "text/plain"` => serializes into `request.text()`
69 changes: 69 additions & 0 deletions .changeset/raw-payload-submission.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
---
"react-router-dom": minor
---

- Support submission of raw payloads through `useSubmit`/`fetcher.submit` by opting out of serialization into `request.formData` using `encType: null`. When opting-out of serialization, your data will be passed to the action in a new `payload` parameter:

```jsx
function Component() {
let submit = useSubmit();
submit({ key: "value" }, { encType: null });
}

function action({ request, payload }) {
// payload => { key: 'value' }
// request.body => null
}
```

Since the default behavior in `useSubmit` today is to serialize to `application/x-www-form-urlencoded`, that will remain the behavior for `encType:undefined` in v6. But in v7, we plan to change the default behavior for `undefined` to skip serialization. In order to better prepare for this change, we encourage developers to add explicit content types to scenarios in which they are submitting raw JSON objects:

```jsx
function Component() {
let submit = useSubmit();

// Change this:
submit({ key: "value" });

// To this:
submit({ key: "value" }, { encType: "application/x-www-form-urlencoded" });
}
```

- You may now also opt-into different types of serialization of this `payload` into your `request` using the `formEncType` parameter:

```js
function Component() {
let submit = useSubmit();
submit({ key: "value" }, { encType: "application/json" });
}

function action({ request, payload }) {
// payload => { key: 'value' }
// await request.json() => {"key":"value"}
}
```

```js
function Component() {
let submit = useSubmit();
submit({ key: "value" }, { encType: "application/x-www-form-urlencoded" });
}

function action({ request, payload }) {
// payload => { key: 'value' }
// await request.formData() => FormData instance with a single entry of key=value
}
```

```js
function Component() {
let submit = useSubmit();
submit("Plain ol' text", { encType: "text/plain" });
}

function action({ request, payload }) {
// payload => "Plain ol' text"
// await request.text() => "Plain ol' text"
}
```
10 changes: 10 additions & 0 deletions docs/hooks/use-fetcher.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ function SomeComponent() {
// build your UI with these properties
fetcher.state;
fetcher.formData;
fetcher.payload;
fetcher.formMethod;
fetcher.formAction;
fetcher.data;
Expand Down Expand Up @@ -132,6 +133,8 @@ export function useIdleLogout() {
}
```

`fetcher.submit` is a wrapper around a [`useSubmit`][use-submit] call for the fetcher instance, so it also accepts the same options as `useSubmit`.

If you want to submit to an index route, use the [`?index` param][indexsearchparam].

If you find yourself calling this function inside of click handlers, you can probably simplify your code by using `<fetcher.Form>` instead.
Expand Down Expand Up @@ -200,6 +203,8 @@ function TaskCheckbox({ task }) {
}
```

If you opt-out of serialization using `encType: null`, then `fetcher.formData` will be `undefined` and your data will be exposed on `fetcher.payload`.

## `fetcher.formAction`

Tells you the action url the form is being submitted to.
Expand All @@ -224,10 +229,15 @@ fetcher.formMethod; // "post"

<docs-warning>The `fetcher.formMethod` field is lowercase without the `future.v7_normalizeFormMethod` [Future Flag][api-development-strategy]. This is being normalized to uppercase to align with the `fetch()` behavior in v7, so please upgrade your React Router v6 applications to adopt the uppercase HTTP methods.</docs-warning>

## `fetcher.payload`

Any POST, PUT, PATCH, or DELETE that started from a `fetcher.submit(payload, { encType: null })` will have your `payload` value represented in `fetcher.payload`.

[loader]: ../route/loader
[action]: ../route/action
[pickingarouter]: ../routers/picking-a-router
[indexsearchparam]: ../guides/index-search-param
[link]: ../components/link
[form]: ../components/form
[api-development-strategy]: ../guides/api-development-strategy
[use-submit]: ./use-submit.md
7 changes: 7 additions & 0 deletions docs/hooks/use-navigation.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ function SomeComponent() {
navigation.state;
navigation.location;
navigation.formData;
navigation.payload;
navigation.formAction;
navigation.formMethod;
}
Expand Down Expand Up @@ -90,8 +91,14 @@ let isRedirecting =

Any POST, PUT, PATCH, or DELETE navigation that started from a `<Form>` or `useSubmit` will have your form's submission data attached to it. This is primarily useful to build "Optimistic UI" with the `submission.formData` [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) object.

If you opt-out of serialization using `encType: null`, then `navigation.formData` will be `undefined` and your data will be exposed on `navigation.payload`.

In the case of a GET form submission, `formData` will be empty and the data will be reflected in `navigation.location.search`.

## `navigation.payload`

Any POST, PUT, PATCH, or DELETE navigation that started from a `useSubmit(payload, { encType: null })` will have your `payload` value represented in `navigation.payload`.

## `navigation.location`

This tells you what the next [location][location] is going to be.
Expand Down
42 changes: 42 additions & 0 deletions docs/hooks/use-submit.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,48 @@ formData.append("cheese", "gouda");
submit(formData);
```

### Payload Serialization

You may also submit raw JSON to your `action` and the default behavior will be to encode the key/values into `FormData`:

```tsx
let obj = { key: "value" };
submit(obj); // -> request.formData()
Copy link
Member

Choose a reason for hiding this comment

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

Should there be additional type-checking for this use case (and I guess when explicitly setting encType: 'application/x-www-form-urlencoded')?

If I understand correctly, it seems like a big footgun that I can now pass any type here and I won't get a type error and then it'll blow up later when the code assumes it can be serialized to 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.

Probably 😬 . #10362 starts adding some more advanced type checks around payload/encType/action so we can try to add it there - but it's proven a bit tricky thus far to get the inferred types working right across params. I'm sure we can figure something out though. I'll drop a note over there

```

You may also choose which type of serialization you'd like via the `encType` option:

```tsx
let obj = { key: "value" };
submit(obj, { encType: 'application/x-www-form-urlencoded' }); // -> request.formData()
```

```tsx
let obj = { key: "value" };
submit(obj, { encType: "application/json" }); // -> request.json()
```

```tsx
let text = "Plain ol' text";
submit(obj, { encType: "text/plain" }); // -> request.text()
```

<docs-warn>In future versions of React Router, the default behavior will not serialize raw JSON payloads. If you are submitting raw JSON today it's recommended to specify an explicit `encType`.</docs-warn>
Copy link
Member

Choose a reason for hiding this comment

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

Have we talked about making this a future flag?

Copy link
Contributor Author

@brophdawg11 brophdawg11 Apr 18, 2023

Choose a reason for hiding this comment

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

Ryan and I talked through this a good bit and landed on a heuristic that I need to get added to our docs and specifically this diagram.

A future flag is needed when there is no way to opt-in to the new (breaking) behavior at the call-site (locally), so we need to give them a config (global) opt-in flag. For example:

  • Deprecating useTransition in favor of useNavigation - this was a breaking change but they can opt-into the new behavior by using the new hook. So we didn't need a future flag and instead can just log a deprecation warning when they call useTransition.
  • Removing fetcher.type/fetcher.submission - this was a breaking change but they can opt-into the new behavior by changing their code to look at fetcher.state instead. So again no future flag needed and instead can just log a deprecation warning when they access fetcher.type/fetcher.submission.
  • v1 -> v2 route conventions - There's no way they can opt into this in the routes/ directory so we needed to provide a future flag.
  • Normalizing fetcher.formMethod from post -> POST - there was no way they could do this at the call site using existing APIs so we had to give them a global future flag.

So that diagram needs a new path for breaking changes where we say "local opt-in possible?" and if so we can just log deprecation warnings and don't need to introduce a future flag.


### Opting out of serialization

Sometimes in a client-side application, it's overkill to require serialization into `request.formData` when you have a raw JSON object in your component and want to submit it to your `action` directly. If you'd like to opt out of serialization, you can pass `encType: null` to your second options argument, and your data will be sent to your action function verbatim as a `payload` parameter:

```tsx
let obj = { key: "value" };
submit(obj, { encType: null });

function action({ request, payload }) {
// payload is `obj` from your component
// request.body === null
}
```

## Submit options

The second argument is a set of options that map directly to form submission attributes:
Expand Down
1 change: 1 addition & 0 deletions docs/route/should-revalidate.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ interface ShouldRevalidateFunction {
formAction?: Submission["formAction"];
formEncType?: Submission["formEncType"];
formData?: Submission["formData"];
payload?: Submission["payload"];
actionResult?: DataResult;
defaultShouldRevalidate: boolean;
}): boolean;
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,10 @@
"none": "15.6 kB"
},
"packages/react-router-dom/dist/react-router-dom.production.min.js": {
"none": "11.8 kB"
"none": "12 kB"
},
"packages/react-router-dom/dist/umd/react-router-dom.production.min.js": {
"none": "17.7 kB"
"none": "17.9 kB"
}
}
}
Loading