Back home

Group and parse fields in Remix / React Router v6

React Router v6 introduced the ability to have route loaders and actions, changing the paradigm for how you perform data mutations with a simple mental model where HTML and HTTP requests are the main characters.

You can leave aside having tons of useState and callbacks to manage the state. If you have been in the development world long enough, this may sound familiar from the good old times.

Let's dive in and imagine you have the following scenario: You have a group of shopping cart items you want to edit, so any cart can have any number of items. It will look something like this:

const items = [
	{ id: 1, name: "Padel racket", quantity: 1, price: 13000 },
	{ id: 2, name: "Black shorts", quantity: 1, price: 4499 },
	{ id: 3, name: "Wilson Rush Pro", quantity: 1, price: 9999 },
	...
];

In order to work with route actions, we only need to set a name for the input and wrap it in a <Form> tag from React Router. The value will be sent to the action, and no state is needed! Pretty awesome.

Form structure

To maintain that structure in the route action, you need to group your fields as "children" of a parent field and give each "child" a numeric index. Yes, like an array of objects, but with HTML naming. Something like this:

import { Form } from "react-router-dom";

// The action URL should be the one where you have the route action
<Form action="/order" method="put">
 {items.map((item, index) => (
	<input type="hidden" name={`cart[${index}][id]`} value={item.id}  />
	<input type="text" name={`cart[${index}][name]`} value={item.name}  />
	<input type="text" name={`cart[${index}][quantity]`} value={quantity}  />
	<input type="text" name={`cart[${index}][price]`} value={item.price}  />
 )}
</Form>

If you need some fields in the route action that are not editable, don't forget about the <input type="hidden" />:

Route action

Now, in the route action, we should handle that input. First of all, you need to install the qs package:

npm install qs

That's because request.formData() is not powerful enough to format the data properly, so you would end up doing some kind of black magic to parse it. qs takes care of all of that.

Now in the route action:

export async function orderEditAction({ request, params }) {
	// Get the request as a text
	const data = await request.text();
	const { cart } = parse(data);

	console.log("OrderEditAction", cart);
}

That's all! Just make sure you have the structure you want represented in the HTML and use qs to parse it.