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

🗺 Alternate submission encodings #10413

Merged
merged 44 commits into from
Jun 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
4d58d0e
Revert "Revert "Add better control over submission serialization (#1…
brophdawg11 Apr 28, 2023
97ec257
Revert "Revert "Support direct handlers in useSubmit/fetcher.submit/f…
brophdawg11 Apr 28, 2023
ac1f5fc
Update router to leverage body instead of payload
brophdawg11 Apr 26, 2023
1bc5cfa
DRY up navigation/fetcher creation
brophdawg11 Apr 26, 2023
68153da
Switch react-router-dom layer over to use body instead of payload
brophdawg11 Apr 28, 2023
3337a52
Fix up type errors
brophdawg11 Apr 28, 2023
d1783e8
Bundle
brophdawg11 May 1, 2023
beeac89
Remove inline action support from useSubmit
brophdawg11 May 3, 2023
a1b69ac
Change router.fetch API to accept inline action in href slot
brophdawg11 May 3, 2023
737adbf
Remove no-longer-needed FormData serialization from react-router-dom
brophdawg11 May 3, 2023
8c3c94e
Bump bundle
brophdawg11 May 3, 2023
a1e3909
Disallow json + text encTypes from <Form>
brophdawg11 May 3, 2023
9695f7c
Remove changes to data router example
brophdawg11 May 3, 2023
8d34552
Update docs to reflect new APIs
brophdawg11 May 3, 2023
53dbf3a
Docs updates
brophdawg11 May 3, 2023
97989cf
Add tests for memory routers
brophdawg11 May 3, 2023
aafc535
Update unit tests workflows
brophdawg11 May 3, 2023
6456850
Remode node latest since node 20 have issues with react native
brophdawg11 May 3, 2023
fd54df8
Merge branch 'dev' into brophdawg11/better-actions
brophdawg11 May 22, 2023
3a09823
Bump bundle
brophdawg11 May 22, 2023
55e6ef4
Few updates and a TODO or two
brophdawg11 May 23, 2023
4b38668
Fix type error
brophdawg11 May 25, 2023
495d9a2
Remove inline loaders/actions support
brophdawg11 May 30, 2023
56e66e1
Fix some tests
brophdawg11 May 30, 2023
8e27ab4
Drop bundle
brophdawg11 May 30, 2023
f8ddea6
Bump
brophdawg11 May 31, 2023
fe87f25
Updates
brophdawg11 May 31, 2023
fc044bb
Fix lint issues
brophdawg11 May 31, 2023
9a137cc
Don't throw on wrong encTypre and add support for string encoding
brophdawg11 May 31, 2023
ad03283
Bump bundle
brophdawg11 May 31, 2023
004123d
Handle invalid body submissions with a 400 error
brophdawg11 May 31, 2023
32aad46
Bump bundle
brophdawg11 May 31, 2023
8a7df27
MAke text/json/formData mutually exclusive
brophdawg11 Jun 2, 2023
253cca7
Add support for <Form encType=text/plain>
brophdawg11 Jun 2, 2023
64a932a
Merge branch 'dev' into brophdawg11/better-actions
brophdawg11 Jun 2, 2023
f4c7713
Bundle
brophdawg11 Jun 2, 2023
a42d81c
Merge branch 'dev' into brophdawg11/better-actions
brophdawg11 Jun 6, 2023
7c929c5
Merge branch 'dev' into brophdawg11/better-actions
brophdawg11 Jun 8, 2023
ea983e4
Movve text after json
brophdawg11 Jun 8, 2023
f8a71dc
Missed a spot
brophdawg11 Jun 8, 2023
2b586c9
Error on json/text GET submissions
brophdawg11 Jun 8, 2023
d849944
Report fetcher submission errors outward
brophdawg11 Jun 8, 2023
153c054
Fix test
brophdawg11 Jun 8, 2023
e22ec53
Bump bundle
brophdawg11 Jun 8, 2023
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
44 changes: 44 additions & 0 deletions .changeset/raw-payload-submission-router.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
---
"@remix-run/router": minor
---

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`:

```js
// By default, the encoding is "application/x-www-form-urlencoded"
router.navigate("/", {
formMethod: "post",
body: { key: "value" },
});

function action({ request }) {
// request.formData => FormData instance with entry [key=value]
// request.text => "key=value"
}
```

```js
// Pass `formEncType` to opt-into a different encoding
router.navigate("/", {
formMethod: "post",
formEncType: "application/json",
body: { key: "value" },
});

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

```js
router.navigate("/", {
formMethod: "post",
formEncType: "text/plain",
body: "Text submission",
});

function action({ request }) {
// request.text => "Text submission"
}
```
55 changes: 55 additions & 0 deletions .changeset/raw-payload-submission.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
---
"react-router-dom": minor
---

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`).

```jsx
// The default behavior will still serialize as FormData
function Component() {
let navigation = useNavigation();
let submit = useSubmit();
submit({ key: "value" });
// navigation.formEncType => "application/x-www-form-urlencoded"
// navigation.formData => FormData instance
// navigation.text => "key=value"
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Let's consider making text/formData/json all mutually exclusive to avoid buffering large bodies in memory

}

function action({ request }) {
// request.headers.get("Content-Type") => "application/x-www-form-urlencoded"
// request.formData => FormData instance
// request.text => "key=value"
}
```

```js
// Opt-into JSON encoding with `encType: "application/json"`
function Component() {
let submit = useSubmit();
submit({ key: "value" }, { encType: "application/json" });
// navigation.formEncType => "application/json"
// navigation.json => { key: "value" }
// navigation.text => '{"key":"value"}'
}

function action({ request }) {
// request.headers.get("Content-Type") => "application/json"
// request.json => { key: "value" }
// request.text => '{"key":"value"}'
}
```

```js
// Opt-into JSON encoding with `encType: "application/json"`
function Component() {
let submit = useSubmit();
submit("Text submission", { encType: "text/plain" });
// navigation.formEncType => "text/plain"
// navigation.text => "Text submission"
}

function action({ request }) {
// request.headers.get("Content-Type") => "text/plain"
// request.text => "Text submission"
}
```
11 changes: 9 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,14 @@ concurrency:

jobs:
test:
name: 🧪 Test
name: "🧪 Test: (Node: ${{ matrix.node }})"
strategy:
fail-fast: false
matrix:
node:
- 16
- 18

runs-on: ubuntu-latest

steps:
Expand All @@ -33,7 +40,7 @@ jobs:
with:
cache: yarn
check-latest: true
node-version-file: ".nvmrc"
node-version: ${{ matrix.node }}

- name: Disable GitHub Actions Annotations
run: |
Expand Down
13 changes: 13 additions & 0 deletions docs/hooks/use-fetcher.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ function SomeComponent() {
// build your UI with these properties
fetcher.state;
fetcher.formData;
fetcher.json;
fetcher.text;
fetcher.formMethod;
fetcher.formAction;
fetcher.data;
Expand Down Expand Up @@ -132,6 +134,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 +204,14 @@ function TaskCheckbox({ task }) {
}
```

## `fetcher.json`

When using `fetcher.submit(data, { formEncType: "application/json" })`, the submitted JSON is available via `fetcher.json`.

## `fetcher.text`

When using `fetcher.submit(data, { formEncType: "text/plain" })`, the submitted text is available via `fetcher.text`.

## `fetcher.formAction`

Tells you the action url the form is being submitted to.
Expand Down Expand Up @@ -231,3 +243,4 @@ fetcher.formMethod; // "post"
[link]: ../components/link
[form]: ../components/form
[api-development-strategy]: ../guides/api-development-strategy
[use-submit]: ./use-submit.md
10 changes: 10 additions & 0 deletions docs/hooks/use-navigation.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ function SomeComponent() {
navigation.state;
navigation.location;
navigation.formData;
navigation.json;
navigation.text;
navigation.formAction;
navigation.formMethod;
}
Expand Down Expand Up @@ -92,6 +94,14 @@ Any POST, PUT, PATCH, or DELETE navigation that started from a `<Form>` or `useS

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

## `navigation.json`

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`.

## `navigation.text`

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`.

## `navigation.location`

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

Or you can submit `URLSearchParams`:

```tsx
let searchParams = new URLSearchParams();
searchParams.append("cheese", "gouda");
submit(searchParams);
```

Or anything that the `URLSearchParams` constructor accepts:

```tsx
submit("cheese=gouda&toasted=yes");
submit([
["cheese", "gouda"],
["toasted", "yes"],
]);
```

The default behavior if you submit a JSON object is to encode the data into `FormData`:

```tsx
submit({ key: "value" });
// will serialize into request.formData() in your action
```

Or you can opt-into JSON encoding:

```tsx
submit({ key: "value" }, { encType: "application/json" });
// will serialize into request.json() in your action

submit('{"key":"value"}', { encType: "application/json" });
// will encode into request.json() in your action
```

Or plain text:

```tsx
submit("value", { encType: "text/plain" });
// will serialize into request.text() in your action
```

## Submit options

The second argument is a set of options that map directly to form submission attributes:
The second argument is a set of options that map (mostly) directly to form submission attributes:

```tsx
submit(null, {
Expand Down
5 changes: 5 additions & 0 deletions docs/route/action.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@ formData.get("lyrics");

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

### Opt-in serialization types

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()`.

## Returning Responses

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].
Expand Down Expand Up @@ -200,6 +204,7 @@ If a button name/value isn't right for your use case, you could also use a hidde
[form]: ../components/form
[workingwithformdata]: ../guides/form-data
[useactiondata]: ../hooks/use-action-data
[usesubmit]: ../hooks/use-submit
[returningresponses]: ./loader#returning-responses
[createbrowserrouter]: ../routers/create-browser-router
[button]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button
Expand Down
2 changes: 2 additions & 0 deletions docs/route/should-revalidate.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ interface ShouldRevalidateFunction {
formAction?: Submission["formAction"];
formEncType?: Submission["formEncType"];
formData?: Submission["formData"];
json?: Submission["json"];
text?: Submission["text"];
actionResult?: DataResult;
defaultShouldRevalidate: boolean;
}): boolean;
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@
},
"filesize": {
"packages/router/dist/router.umd.min.js": {
"none": "45 kB"
"none": "46.4 kB"
},
"packages/react-router/dist/react-router.production.min.js": {
"none": "13.4 kB"
Expand All @@ -118,10 +118,10 @@
"none": "15.8 kB"
},
"packages/react-router-dom/dist/react-router-dom.production.min.js": {
"none": "12.1 kB"
"none": "12.3 kB"
},
"packages/react-router-dom/dist/umd/react-router-dom.production.min.js": {
"none": "18.1 kB"
"none": "18.3 kB"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import {
waitFor,
} from "@testing-library/react";
import { JSDOM } from "jsdom";
import LazyComponent from "./components//LazyComponent";

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