Skip to content

Commit

Permalink
Alternate submission encodings (#10413)
Browse files Browse the repository at this point in the history
  • Loading branch information
brophdawg11 committed Jun 8, 2023
1 parent f4c7c80 commit c8c417d
Show file tree
Hide file tree
Showing 21 changed files with 1,811 additions and 384 deletions.
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"
}

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

0 comments on commit c8c417d

Please sign in to comment.