Skip to content

Should I use a form library in Remix?

Posted on:December 16, 2023

Remix has a great story for forms. I remember clearly a series of tweets by Ryan even before Remix was a thing, where he re-introduced HTML forms to the React community.

It was a revelation to see a form working without preventDefault and all those state management workarounds. It enabled us to just use the plaform.

No extra code. No extra dependencies. No extra headaches.

Yet it looks like everybody’s using some kind of a form library.

Why are they doing it? And should you do it as well?

Why would you consider library for forms?

At their core, forms are simple. They are a declarative way to pass data with intent to the server. Intent being the form’s action and method, and the data being the inputs.

Just place the form in your HTML and have a controller to handle it.

export default function SomeForm() {
return (
<Form method="post">
<input name="age" />
<select name="favoriteDrink">
<option>water</option>
<option>soda</option>
<option>beer</option>
<option>wine</option>
</select>
<button type="submit">Send</button>
</Form>
)
}
export async function action({ request }) {
const formData = await requsst.formData();
const age = formData.get('age');
// do something
}

It is very easy for complexity to creep in though. Before you know it you might need to:

In a blink of an eye this simple form becomes this a bloated spaghetti monster.

function validateForm(formData) {
const age = formData.get("age");
const favoriteDrink = formData.get("favoriteDrink");
const errors = { age: null, favoriteDrink: null };
if (typeof age !== "number") errors.age = "Age must be a number";
if (age < 5) errors.age = "Too young";
if (age > 120) errors.age = "Wow! impressive! But too old";
if (!["water", "soda", "beer", "wine"].includes(favoriteDrink))
errors.favoriteDrink = "Drink not in list";
if (age < 21 && ["beer", "wine"].includes(favoriteDrink))
errors.favoriteDrink = "Whoops! nice try though";
return {
success: !erros.age && !errors.favoriteDrink,
errors,
};
}
export default function SomeForm() {
const [age, setAge] = useState(0);
const [errors, setErrors] = useState({ age: null, favoriteDrink: null });
const actionData = useActionData();
useEffect(() => {
if(actionData) {
setErrors(actionData)
}
}, [actionData])
function handleSubmit(event) {
const formData = new FormData(event.target);
const validationResults = validateForm(formData);
if (!validationResults.success) {
event.preventDefault();
setErrors(validationResults.errors);
}
}
return (
<Form method="post" onSubmit={handleSubmit}>
<input type="number" name="age" onChange={(e) => setAge(e.target.value)} value={age} />
{errors.age}
<select name="favoriteDrink">
<option>water</option>
<option>soda</option>
{age >= 21 && (
<>
<option>beer</option>
<option>wine</option>
</>
)}
</select>
{errors.favoriteDrink}
<button type="submit">Send</button>
</Form>
);
}
export async function action({ request }) {
const formData = await requsst.formData();
const validationResults = validateForm(formData);
if (!validationResults.success) {
return json(validationResults.errors, { status: 400 });
}
// do something
}

And this is just the beginning!

This is where form libraries come into play. They help you to manage the complexity in a unified and documented way so that you and your team won’t need to do it yourself.

Do you need a form library?

After all we talked about you might think that jumping into the forms library bandwagon is always the right choice. In most cases it certainly is! But as always in software… id depends.

Here are a few points to consider.

If you are still not sure. I would say start without, and introduce one if neccessary :)



If you found it useful, I will be happy to hear about it :)