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

Conversation

brophdawg11
Copy link
Contributor

@brophdawg11 brophdawg11 commented Apr 13, 2023

Depends on #10336

Closes part of #10324 (does not yet handle call-site/override actions - that will be a separate PR):

  • Adds support for useSubmit()(obj, { encType: null }) to opt out of serialization for raw payloads. These are then passed to the action via the payload parameter. Same goes for fetcher.submit.
  • Add support for opt-in serialization of payloads for encType:application/json and encType:text/plain

See the changesets for examples.

@changeset-bot
Copy link

changeset-bot bot commented Apr 13, 2023

🦋 Changeset detected

Latest commit: d8dc40a

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 5 packages
Name Type
@remix-run/router Minor
react-router-dom Minor
react-router Minor
react-router-dom-v5-compat Minor
react-router-native Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@brophdawg11 brophdawg11 self-assigned this Apr 13, 2023
Base automatically changed from brophdawg11/stable-navigate to dev April 14, 2023 19:08
@brophdawg11 brophdawg11 force-pushed the brophdawg11/opt-out-serialization branch from 6608193 to c945785 Compare April 14, 2023 19:19
| FormData
| URLSearchParams
| { [name: string]: string }
| NonNullable<unknown> // Raw payload submissions
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We can now submit anything other than undefined as our payload

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@ryanflorence - Mark brought up a good point that this could potentially open up a footgun for users thinking anything can be serialized into FormData. With encType:null we can accept anything (and rule out undefined for clarity).

But without encType not everything can be encoded into FormData

// Technically arrays are fine and serialize into formData as a map of index => value
submit([1,2,3])

// but strings will blow up without any type hints
submit("plain text")

// as will nested objects and other complex structures
submit({ parent: { child: { key: "value" } } })

Right now the plan is to handle any advanced type inference in a new PR so we can tackle it there based on what we decide. We could also just log a warning if you submit a non-FormData serializable value?

Comment on lines 441 to 442
| { formData: FormData; payload?: undefined }
| { formData?: undefined; payload: any }
Copy link
Contributor Author

Choose a reason for hiding this comment

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

formData/payload are mutually exclusive on the navigation- you can only include one in a call to router.navigate

Comment on lines +3731 to +3766
if (formData) {
if (formEncType === "application/x-www-form-urlencoded") {
// Content-Type is inferred (https://fetch.spec.whatwg.org/#dom-request)
init.body = convertFormDataToSearchParams(formData);
} else {
// Content-Type is inferred (https://fetch.spec.whatwg.org/#dom-request)
init.body = formData;
}
} else if (payload != null) {
if (formEncType === "application/x-www-form-urlencoded") {
let payloadFormData = new FormData();

if (payload instanceof URLSearchParams) {
for (let [name, value] of payload) {
payloadFormData.append(name, value);
}
} else if (payload != null) {
for (let name of Object.keys(payload)) {
// @ts-expect-error
payloadFormData.append(name, payload[name]);
}
}

// Content-Type is inferred (https://fetch.spec.whatwg.org/#dom-request)
init.body = convertFormDataToSearchParams(payloadFormData);
} else if (formEncType === "application/json") {
init.headers = new Headers({ "Content-Type": formEncType });
init.body = JSON.stringify(payload);
} else if (formEncType === "text/plain") {
init.headers = new Headers({
"Content-Type": formEncType,
});
init.body =
typeof payload === "string" ? payload : JSON.stringify(payload);
}
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Serialize the incoming formData or payload onto the request if we know how

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@ryanflorence Tagging for review - new router serialization based on content-type

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

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.

@@ -147,7 +147,7 @@ export interface Router {
key: string,
routeId: string,
href: string | null,
opts?: RouterNavigateOptions
opts?: RouterFetchOptions
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was an underlying type bug, but unnoticed since they're effectively identical. The changes below properly remove both replace/state from fetch() options


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

Co-authored-by: Mark Dalgleish <mark.john.dalgleish@gmail.com>
Comment on lines +286 to +289
// When a raw object is sent - even though we encode it into formData,
// we still expose it as payload so it aligns with the behavior if
// encType were application/json or text/plain
payload = target;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@ryanflorence I went slightly against the mutual exclusivity here so that application/x-www-form-urlencoded would better mimic the behavior of application/json and text/plain.

formData and payload remain mutually exclusive when submitting an HTMLElement, FormData, or URLSearchParams instance. But not when you submit a raw object.

@brophdawg11 brophdawg11 merged commit 9d81bf6 into dev Apr 21, 2023
2 checks passed
@brophdawg11 brophdawg11 deleted the brophdawg11/opt-out-serialization branch April 21, 2023 18:54
brophdawg11 added a commit that referenced this pull request Apr 26, 2023
@github-actions
Copy link
Contributor

🤖 Hello there,

We just published version 6.11.0-pre.0 which includes this pull request. If you'd like to take it for a test run please try it out and let us know what you think!

Thanks!

@brophdawg11
Copy link
Contributor Author

Silly bot, these were reverted from dev so they're not in 6.11.0-pre.0. We plan to get them re-added with the latest changes from the Proposal in 6.12.

brophdawg11 added a commit that referenced this pull request Apr 28, 2023
@remix-run remix-run deleted a comment from github-actions bot Apr 28, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants