Skip to content

Commit c8c417d

Browse files
authored
Alternate submission encodings (#10413)
1 parent f4c7c80 commit c8c417d

21 files changed

+1811
-384
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
---
2+
"@remix-run/router": minor
3+
---
4+
5+
Add support for `application/json` and `text/plain` encodings for `router.navigate`/`router.fetch` submissions. To leverage these encodings, pass your data in a `body` parameter and specify the desired `formEncType`:
6+
7+
```js
8+
// By default, the encoding is "application/x-www-form-urlencoded"
9+
router.navigate("/", {
10+
formMethod: "post",
11+
body: { key: "value" },
12+
});
13+
14+
function action({ request }) {
15+
// request.formData => FormData instance with entry [key=value]
16+
// request.text => "key=value"
17+
}
18+
```
19+
20+
```js
21+
// Pass `formEncType` to opt-into a different encoding
22+
router.navigate("/", {
23+
formMethod: "post",
24+
formEncType: "application/json",
25+
body: { key: "value" },
26+
});
27+
28+
function action({ request }) {
29+
// request.json => { key: "value" }
30+
// request.text => '{ "key":"value" }'
31+
}
32+
```
33+
34+
```js
35+
router.navigate("/", {
36+
formMethod: "post",
37+
formEncType: "text/plain",
38+
body: "Text submission",
39+
});
40+
41+
function action({ request }) {
42+
// request.text => "Text submission"
43+
}
44+
```

.changeset/raw-payload-submission.md

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
---
2+
"react-router-dom": minor
3+
---
4+
5+
Add support for `application/json` and `text/plain` encodings for `useSubmit`/`fetcher.submit`. To reflect these additional types, `useNavigation`/`useFetcher` now also contain `navigation.json`/`navigation.text` and `fetcher.json`/`fetcher.text` which are getter functions mimicking `request.json` and `request.text`. Just as a `Request` does, if you access one of these methods for the incorrect encoding type, it will throw an Error (i.e. accessing `navigation.formData` when `navigation.formEncType` is `application/json`).
6+
7+
```jsx
8+
// The default behavior will still serialize as FormData
9+
function Component() {
10+
let navigation = useNavigation();
11+
let submit = useSubmit();
12+
submit({ key: "value" });
13+
// navigation.formEncType => "application/x-www-form-urlencoded"
14+
// navigation.formData => FormData instance
15+
// navigation.text => "key=value"
16+
}
17+
18+
function action({ request }) {
19+
// request.headers.get("Content-Type") => "application/x-www-form-urlencoded"
20+
// request.formData => FormData instance
21+
// request.text => "key=value"
22+
}
23+
```
24+
25+
```js
26+
// Opt-into JSON encoding with `encType: "application/json"`
27+
function Component() {
28+
let submit = useSubmit();
29+
submit({ key: "value" }, { encType: "application/json" });
30+
// navigation.formEncType => "application/json"
31+
// navigation.json => { key: "value" }
32+
// navigation.text => '{"key":"value"}'
33+
}
34+
35+
function action({ request }) {
36+
// request.headers.get("Content-Type") => "application/json"
37+
// request.json => { key: "value" }
38+
// request.text => '{"key":"value"}'
39+
}
40+
```
41+
42+
```js
43+
// Opt-into JSON encoding with `encType: "application/json"`
44+
function Component() {
45+
let submit = useSubmit();
46+
submit("Text submission", { encType: "text/plain" });
47+
// navigation.formEncType => "text/plain"
48+
// navigation.text => "Text submission"
49+
}
50+
51+
function action({ request }) {
52+
// request.headers.get("Content-Type") => "text/plain"
53+
// request.text => "Text submission"
54+
}
55+
```

.github/workflows/test.yml

+9-2
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,14 @@ concurrency:
2121

2222
jobs:
2323
test:
24-
name: 🧪 Test
24+
name: "🧪 Test: (Node: ${{ matrix.node }})"
25+
strategy:
26+
fail-fast: false
27+
matrix:
28+
node:
29+
- 16
30+
- 18
31+
2532
runs-on: ubuntu-latest
2633

2734
steps:
@@ -33,7 +40,7 @@ jobs:
3340
with:
3441
cache: yarn
3542
check-latest: true
36-
node-version-file: ".nvmrc"
43+
node-version: ${{ matrix.node }}
3744

3845
- name: Disable GitHub Actions Annotations
3946
run: |

docs/hooks/use-fetcher.md

+13
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ function SomeComponent() {
3838
// build your UI with these properties
3939
fetcher.state;
4040
fetcher.formData;
41+
fetcher.json;
42+
fetcher.text;
4143
fetcher.formMethod;
4244
fetcher.formAction;
4345
fetcher.data;
@@ -132,6 +134,8 @@ export function useIdleLogout() {
132134
}
133135
```
134136

137+
`fetcher.submit` is a wrapper around a [`useSubmit`][use-submit] call for the fetcher instance, so it also accepts the same options as `useSubmit`.
138+
135139
If you want to submit to an index route, use the [`?index` param][indexsearchparam].
136140

137141
If you find yourself calling this function inside of click handlers, you can probably simplify your code by using `<fetcher.Form>` instead.
@@ -200,6 +204,14 @@ function TaskCheckbox({ task }) {
200204
}
201205
```
202206

207+
## `fetcher.json`
208+
209+
When using `fetcher.submit(data, { formEncType: "application/json" })`, the submitted JSON is available via `fetcher.json`.
210+
211+
## `fetcher.text`
212+
213+
When using `fetcher.submit(data, { formEncType: "text/plain" })`, the submitted text is available via `fetcher.text`.
214+
203215
## `fetcher.formAction`
204216

205217
Tells you the action url the form is being submitted to.
@@ -231,3 +243,4 @@ fetcher.formMethod; // "post"
231243
[link]: ../components/link
232244
[form]: ../components/form
233245
[api-development-strategy]: ../guides/api-development-strategy
246+
[use-submit]: ./use-submit.md

docs/hooks/use-navigation.md

+10
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ function SomeComponent() {
2323
navigation.state;
2424
navigation.location;
2525
navigation.formData;
26+
navigation.json;
27+
navigation.text;
2628
navigation.formAction;
2729
navigation.formMethod;
2830
}
@@ -92,6 +94,14 @@ Any POST, PUT, PATCH, or DELETE navigation that started from a `<Form>` or `useS
9294

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

97+
## `navigation.json`
98+
99+
Any POST, PUT, PATCH, or DELETE navigation that started from a `useSubmit(payload, { encType: "application/json" })` will have your JSON value available in `navigation.json`.
100+
101+
## `navigation.text`
102+
103+
Any POST, PUT, PATCH, or DELETE navigation that started from a `useSubmit(payload, { encType: "text/plain" })` will have your text value available in `navigation.text`.
104+
95105
## `navigation.location`
96106

97107
This tells you what the next [location][location] is going to be.

docs/hooks/use-submit.md

+43-1
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,51 @@ formData.append("cheese", "gouda");
7878
submit(formData);
7979
```
8080

81+
Or you can submit `URLSearchParams`:
82+
83+
```tsx
84+
let searchParams = new URLSearchParams();
85+
searchParams.append("cheese", "gouda");
86+
submit(searchParams);
87+
```
88+
89+
Or anything that the `URLSearchParams` constructor accepts:
90+
91+
```tsx
92+
submit("cheese=gouda&toasted=yes");
93+
submit([
94+
["cheese", "gouda"],
95+
["toasted", "yes"],
96+
]);
97+
```
98+
99+
The default behavior if you submit a JSON object is to encode the data into `FormData`:
100+
101+
```tsx
102+
submit({ key: "value" });
103+
// will serialize into request.formData() in your action
104+
```
105+
106+
Or you can opt-into JSON encoding:
107+
108+
```tsx
109+
submit({ key: "value" }, { encType: "application/json" });
110+
// will serialize into request.json() in your action
111+
112+
submit('{"key":"value"}', { encType: "application/json" });
113+
// will encode into request.json() in your action
114+
```
115+
116+
Or plain text:
117+
118+
```tsx
119+
submit("value", { encType: "text/plain" });
120+
// will serialize into request.text() in your action
121+
```
122+
81123
## Submit options
82124

83-
The second argument is a set of options that map directly to form submission attributes:
125+
The second argument is a set of options that map (mostly) directly to form submission attributes:
84126

85127
```tsx
86128
submit(null, {

docs/route/action.md

+5
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,10 @@ formData.get("lyrics");
101101

102102
For more information on `formData` see [Working with FormData][workingwithformdata].
103103

104+
### Opt-in serialization types
105+
106+
Note that when using [`useSubmit`][usesubmit] you may also pass `encType: "application/json"` or `encType: "text/plain"` to instead serialize your payload into `request.json()` or `request.text()`.
107+
104108
## Returning Responses
105109

106110
While you can return anything you want from an action and get access to it from [`useActionData`][useactiondata], you can also return a web [Response][response].
@@ -200,6 +204,7 @@ If a button name/value isn't right for your use case, you could also use a hidde
200204
[form]: ../components/form
201205
[workingwithformdata]: ../guides/form-data
202206
[useactiondata]: ../hooks/use-action-data
207+
[usesubmit]: ../hooks/use-submit
203208
[returningresponses]: ./loader#returning-responses
204209
[createbrowserrouter]: ../routers/create-browser-router
205210
[button]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button

docs/route/should-revalidate.md

+2
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ interface ShouldRevalidateFunction {
6666
formAction?: Submission["formAction"];
6767
formEncType?: Submission["formEncType"];
6868
formData?: Submission["formData"];
69+
json?: Submission["json"];
70+
text?: Submission["text"];
6971
actionResult?: DataResult;
7072
defaultShouldRevalidate: boolean;
7173
}): boolean;

package.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@
109109
},
110110
"filesize": {
111111
"packages/router/dist/router.umd.min.js": {
112-
"none": "45 kB"
112+
"none": "46.4 kB"
113113
},
114114
"packages/react-router/dist/react-router.production.min.js": {
115115
"none": "13.4 kB"
@@ -118,10 +118,10 @@
118118
"none": "15.8 kB"
119119
},
120120
"packages/react-router-dom/dist/react-router-dom.production.min.js": {
121-
"none": "12.1 kB"
121+
"none": "12.3 kB"
122122
},
123123
"packages/react-router-dom/dist/umd/react-router-dom.production.min.js": {
124-
"none": "18.1 kB"
124+
"none": "18.3 kB"
125125
}
126126
}
127127
}

packages/react-router-dom/__tests__/concurrent-mode-navigations-test.tsx

-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import {
1919
waitFor,
2020
} from "@testing-library/react";
2121
import { JSDOM } from "jsdom";
22-
import LazyComponent from "./components//LazyComponent";
2322

2423
describe("Handles concurrent mode features during navigations", () => {
2524
function getComponents() {

0 commit comments

Comments
 (0)