-
-
Notifications
You must be signed in to change notification settings - Fork 10.3k
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
Conversation
🦋 Changeset detectedLatest commit: d8dc40a The changes in this PR will be included in the next version bump. This PR includes changesets to release 5 packages
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 |
6608193
to
c945785
Compare
| FormData | ||
| URLSearchParams | ||
| { [name: string]: string } | ||
| NonNullable<unknown> // Raw payload submissions |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
packages/router/router.ts
Outdated
| { formData: FormData; payload?: undefined } | ||
| { formData?: undefined; payload: any } |
There was a problem hiding this comment.
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
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); | ||
} | ||
} |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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> |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 ofuseNavigation
- 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 calluseTransition
. - Removing
fetcher.type
/fetcher.submission
- this was a breaking change but they can opt-into the new behavior by changing their code to look atfetcher.state
instead. So again no future flag needed and instead can just log a deprecation warning when they accessfetcher.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
frompost -> 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 |
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 <[email protected]>
// 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; |
There was a problem hiding this comment.
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.
This reverts commit 9d81bf6.
🤖 Hello there, We just published version Thanks! |
Silly bot, these were reverted from |
)"" This reverts commit f92aa2e.
Depends on #10336Closes part of #10324 (does not yet handle call-site/override actions - that will be a separate PR):
useSubmit()(obj, { encType: null })
to opt out of serialization for raw payloads. These are then passed to theaction
via thepayload
parameter. Same goes forfetcher.submit
.encType:application/json
andencType:text/plain
See the changesets for examples.