remix
remix copied to clipboard
feat(remix-react): emulate put/patch/delete when JS is unavailable
References #4420
- [x] Docs
- [x] Tests
Ensure <Form method=...>
behaves the same with and without JavaScript. Although native forms don't support PUT/PATCH/DELETE, we can emulate them by doing a POST and injecting a _method
parameter into the action URL. The Remix server runtime then provides action handlers a Request
with the overridden method
so they are none the wiser.
This provides a more reliable way to use these methods, which can be more ergonomic to write and handle than hidden inputs or buttons with values.
Note:
The emulation only works for action requests handled by Remix; if the form is submitted to another endpoint, it would need to handle the _method
URL parameter accordingly.
🦋 Changeset detected
Latest commit: 18d63514a82967c2181e8eee73e99ec7569a7690
The changes in this PR will be included in the next version bump.
This PR includes changesets to release 17 packages
Name | Type |
---|---|
@remix-run/react | Minor |
@remix-run/server-runtime | Minor |
@remix-run/testing | Minor |
@remix-run/cloudflare | Minor |
@remix-run/deno | Minor |
@remix-run/dev | Minor |
@remix-run/node | Minor |
@remix-run/cloudflare-pages | Minor |
@remix-run/cloudflare-workers | Minor |
create-remix | Minor |
@remix-run/architect | Minor |
@remix-run/express | Minor |
@remix-run/netlify | Minor |
@remix-run/vercel | Minor |
@remix-run/serve | Minor |
remix | Minor |
@remix-run/eslint-config | 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
I titled this as a feat
and bumped the minor versions accordingly, but I could see it being classified differently, lmk what you think...
- It could be argued this is just a bug fix, since we're making the non-JS behavior consistent with the JS behavior
- It could be argued this is a breaking change, since maaaaaaybe someone out there is intentionally handling the
JS-disabled-<Form>
case differently on the server side (e.g. if their action gets aPOST
instead of their desired method, they might render a message that says "you need to enable JavaScript", which would stop "working" with this change 🙃 )
Is it possible to make it work with <button formmethod="put|patch|delete" />
?
@znycheporuk there are a couple big hurdles:
- Browsers don't support setting the other form methods on a button. When you do
<button ...
, React is essentially runningdocument.createElement("button")
and then setting attributes on it. If you set aformmethod
value other thanget
orpost
, the browser silently ignores it, which means we have no way of getting it later. - Even if we could get the other form methods to stick on the button element somehow, when the form is submitted without/before JavaScript, it would not get used because browsers don't support that at submission time.
It may be possible to emulate it a different way, but that would require more plumbing and likely not be worth the trouble. One idea might be for remix to provide a Button
component along these lines:
function Button({ formmethod, name, value, ...props }) {
if (["put", "patch", "delete"].includes(formmethod)) {
name = "_method"
value = formmethod
}
return <button name={name} value={value} {...props} />
}
A downside is that you couldn't use an explicit name+value in conjunction with such a formmethod
, since they would be overridden. Additionally, the server-side _method
logic would need to be adjusted to also look in formData
(multipart or url-encoded), which would make the code more complex and may have performance implications.
I suspect it's probably not worth the trouble, and there may be a much simpler alternative if you want to associate a custom form method with a specific button: Use multiple forms :) ... for example, instead of this:
<Form>
<input name="todoItem" defaultValue={myTodo} />
<button formmethod="put">Update</button>
<button formmethod="delete">Delete</button>
</Form>
You could structure your elements like so:
<>
<Form method="put">
<input name="todoItem" defaultValue={myTodo} />
<button>Update</button>
</Form>
<Form method="delete">
<button>Delete</button>
</Form>
</>
Or if for some reason you really want the buttons in the same form (e.g. to facilitate styling), you can associate it with another form:
<>
<Form method="put">
<input name="todoItem" defaultValue={myTodo} />
<button>Update</button>
<button form="delete-form">Delete</button>
</Form>
<Form method="delete" style={{ display: "none" }} id="delete-form"></Form>
</>
If neither of those approaches work or your use case (e.g. you need all buttons to submit all the form data), then I'd suggest instead using buttons like <button name="intent" value="update">
, and keying off of the intent
value on the server side.
As explained in https://github.com/remix-run/remix/discussions/4420#discussioncomment-4412645, I don't think it's a good idea to make non-JS & JS work the same in this case
I'm personally in favor of dropping non-GET/POST tbh, as that makes us work closer to the spec. Although it's maybe not that much discouraged in the current
Form.method
docs, we did make sure all other docs use GET/POST & set the submitbutton
'sname
to'intent'
if necessary not that long ago.Not discouraging using non-GET/POST would make people also want to use
<button formmethod="delete|patch|put" />
, which is something we'll never be able to support without a customButton
component on our end & some crazy logic on our end (as perfectly explained by @jenseng above & in https://github.com/remix-run/remix/pull/4496#issuecomment-1320222238) We have<button name="intent" value="update">...</button>
to support these kind of things anyways, so I would rather make sure the docs are more pushing towards that instead tbh.So what I think we should do is:
- Keep the current support for non-GET/POST, but add a deprecation warning when people are using them
- Deprecate non-GET/POST in v2
I agree that this would mean non-JS isn't working the same as when JS is enabled, which is something we should fix in most cases. I do however think we shouldn't fix it in this case, as that would encourage using non-GET/POST more, which is (as I explained above) not a good thing imo.
Happy to rebase/revisit this if we can get consensus from the Remix team... do we 1. want to move forward with emulation or 2. just want to deprecate those methods in v2 (or later?)
I think there are good arguments to be made for and against either approach. My personal preference is emulation, but either option would be preferable to the inconsistent status quo.
I think this should be considered in conjunction with https://github.com/remix-run/react-router/discussions/10324#discussioncomment-5734655. I think the crux of the question is "Should any version of <Form>
work when JS is not available"?
One one hand, it would be nice if it could - and in this specific case we could do it with the approach here and it would "just work" with the users action
.
If we introduce <Form encType="application/json">
as stated in the above "better actions" proposal, then we've given the user a way to render a <Form>
that won't work without JS since their action
will read from request.json
which will never be populated without JS. It's opt-in so I think we would just need to be clear in the docs that with that option you're opting out of PE.
But generally I think we should stay consistent. Either <Form>
attrs align directly with underlying browser-supported <form>
attrs (and you useSubmit
if you need/want to opt-out of PE), or they don't and we clearly document which attr values will opt-out of PE.
Definitely will need to get @mjackson and @ryanflorence to weigh in on a choice here.