Better actions with opt-in serialization of payload data #10324
Replies: 6 comments 11 replies
-
This proposal supersedes #9858 |
Beta Was this translation helpful? Give feedback.
-
Thanks @ryanflorence and @brophdawg11 for chatting with me earlier today to help me understand what's going on here. I think I agree with about 90% of what's going on here. There's just one part that bothers me, and it's the I'd like to propose the following modifications:
I feel like these tiny changes stay closer with our current abstraction (the fetch API) and help us avoid API sprawl. Example: submitting JSON object to an actionfunction Comp() {
let submit = useSubmit();
let navigation = useNavigation();
// navigation.json is *the same object* that was given in submit(obj). No need
// to serialize in a purely client-side app.
console.log(navigation.json);
useEffect(() => {
submit(
{ foo: "bar" },
{
// action can be an arbitrary action function, no need for a route
action: someAction,
// treat the payload as JSON (this does not imply that we must *serialize*
// the payload. it just indicates the user's intent: "treat this as JSON")
encType: "application/json",
}
);
}, []);
// ...
} Example: submitting JSON string to an actionThis is similar to Ryan's "opting into serialization" section above. The user may opt in to serialization by function Comp() {
let submit = useSubmit();
let navigation = useNavigation();
// navigation.json is a JSON.parse'd version of the thing they gave us in submit(str)
console.log(navigation.json);
useEffect(() => {
submit(
// Opt-in to serialization by doing it yourself in the case of application/json
JSON.stringify({ foo: "bar" }),
{
// action can be an arbitrary action function, no need for a route
action: someAction,
// treat the payload as JSON (this does not imply that we must *serialize*
// the payload. it just gives us its type)
encType: "application/json",
}
);
}, []);
// ...
} The internal heuristics of the router would basically be: if the Example: fetcherThe fetcher use case is similarly simple to Ryan's except you need to use an let fetcher = useFetcher();
fetcher.submit({ name, projectId }, { action: someAction, encType: "application/json" });
fetcher.json; Example: an actionasync function someAction({ request } /* no additional payload arg needed here */) {
let type = request.headers.get("Content-Type");
// the payload is serialized into the request body now
switch (type) {
case "application/json": {
// this may return either the JSON.parse'd version of a serialized body or the
// raw value that was passed to submit(obj)
let json = await request.json();
}
case "application/x-www-form-urlencoded": {
let formData = await request.formData();
}
case "text/plain": {
let text = await request.text();
}
// invalid! Content-Type must always be defined
//case null: {}
}
} Implementation notes
|
Beta Was this translation helpful? Give feedback.
-
OK so if I had to summarize what this proposal is adding I'd say two things:
|
Beta Was this translation helpful? Give feedback.
-
#10413 is merged to A few notes on the final landing spot for the implementation:
|
Beta Was this translation helpful? Give feedback.
-
Has it been pushed to |
Beta Was this translation helpful? Give feedback.
-
great job on this, when do you think we can get inline loaders/actions? |
Beta Was this translation helpful? Give feedback.
-
Better Actions
Because React Router doesn't actually go to the network, there's no reason to serialize the form values, force
FormData
, or require a route with a path in order to take advantage of the rest of the mutation flow (with pending states, optimistic UI, revalidation, and error/race conditions handling).This enforcement of
FormData
(de)serialization makes adoption of the router's mutation workflows difficult, and the only justification for the friction is "it'll make migrating to Remix easier". While we'd love for many React Router apps to get the benefits of migrating to Remix, the APIs in Router shouldn't artificially enforce the same constraints as a full-stack application.Proposal
encType: null
indicate you don't want to serialize it into to the request body sent to the actionnull
to avoid a breaking change to the current behavior ofencType: undefined
which serializes by defaultencType: undefined
to not serialize and you can opt into serialization viaencType: "application/x-www-form-urlencoded"
action
tosubmit()
andfetcher.submit()
instead of just route pathsBecause React Router doesn't actually go to the network, there's no reason to serialize the values to go over the network, they'll come straight to the action on the second argument:
It's also visible to the type system, so you can have type safety:
The payload will be available to other places as well:
In other words, wherever
formData
shows up,payload
will be there too. They are mutually exclusive, you won't ever have both.Fetchers
Same story for fetchers:
Opt-in to serialization
If an enctype is defined, the value will be encoded into the request body that goes to the action:
Now the value will be serialized into the request body:
This allows code to be shared between React Router, Remix, and anything else that operates on the Web Fetch API, but no longer forces it.
Remix
This sets the foundation to allow
application/json
forsubmit
andfetcher.submit
in Remix apps. While that will break "progressive enhancement by default", it's opt-in, and many applications could benefit greatly from usingrequest.json()
instead of messing withformData
(in my experience, particularly graphqlAPIs where the data you get and post are deeply nested)Beta Was this translation helpful? Give feedback.
All reactions