In Remix, submitting a form is how you make non-GET HTTP requests.
I love this. Writing my own network requests (and storing the results, handling failure, etc) was always annoying. Forms are simple.
However, using forms like, while how nature intended for them to be used, brings up a complication. A Remix route can only have one action, so how do we handle many forms submitting to it?
Let’s make this concrete. Imagine a UI with a button to mark all todos as completed, and another to just delete them all.
There are two distinct actions, so two forms.
An easy way to handle this is to include a hidden input in each “form” that gives a name to each one.1
<div>
<Form method="post">
{/* Form name here! */}
<input type="hidden" name="form" value="mark_all_completed" />
<button type="submit">
Mark all complete
</button>
</Form>
<Form method="post">
{/* And the other form name! */}
<input type="hidden" name="form" value="delete_all" />
<button type="submit">
Just delete 'em
</button>
</Form>
</div>
And then your Remix action can know which form it’s handling.
export async function action({request}: ActionFunctionArgs) {
// Get the name of the form.
const formData = await request.formData();
const form = formData.get('form');
// Handle each form.
switch (form) {
case 'mark_all_completed': {
// ...
break;
}
case 'delete_all': {
// ...
break;
}
}
return null;
}
I call this the Action Reducer pattern (because the switch statement reminds me of how we used to write Redux reducers back in the day).
Let me know if you find this useful, or what techniques you use to make network requests in your Remix apps.
Want to work with me? Need a frontend expert to consult with your team? Check out landslide.software.
The Remix docs actually reference 2 ways to handle this
Adding an “intent” field to the button element, which is the same concept as what I’m describing here. I prefer the hidden input, though, because it doesn’t work for non-buttons (e.g. submitting a form when checking a checkbox). Also, the conform library uses a button’s intent, so I think setting it could mess it up.
Submitting the form to a different route’s action. This is useful if another action is already doing what you need. There’s also something to be said, though, for co-locating the form and action, though.