Form Mate
A tiny and elegant library to handle forms with React:
import Form from "form-mate";
// { fullname: "Francisco", email: "[email protected]" }
export default () => (
<Form onSubmit={(data) => console.log(data)}>
<input name="fullname" required />
<input name="email" type="email" required />
<button>Subscribe!</button>
</Form>
);
Benefits over a plain <form>
:
- Parse the form fields into an object for easy access and consumption. Now you can do
onSubmit(item => setItems([...items, item]))
. - Disable the form while it's submitting to avoid double-submit.
- Prop
onError
for easy error handling, integrates well with other options. - Prop
onChange
to listen to the form changes as they happen. - Prop
autoReset
to clear the form afteronSubmit
finishes successfully. - Prop
encType
(likeencType="multipart/form-data"
) for file handling. It makes the callback receive an instance ofFormData
instead of a plain object. This makes it easy to submit files withfetch()
, Axios, etc. - Sub Component
<FormError />
for more advanced error management. - Sub Component
<FormLoading />
for more advanced loading state handling.
Getting Started
Install form-mate
with npm:
npm install form-mate
Import it and use it anywhere in your React project:
import Form from "form-mate";
export default () => (
<Form onSubmit={(data) => console.log(data)} autoReset>
{/* ... */}
</Form>
);
API
onSubmit
Mandatory prop that accepts a sync or async
callback. It will receive the values in the form when submitted:
import Form from "form-mate";
export default function Subscribe() {
return (
<Form onSubmit={(data) => console.log(data)}>
<input name="name" defaultValue="Francisco" />
<input name="subscribe" defaultChecked type="checkbox" />
<input name="terms" value="accepted" defaultChecked type="checkbox" />
<input name="gender" value="female" type="radio" />
<input name="gender" value="male" defaultChecked type="radio" />
<button>Subscribe!</button>
</Form>
);
}
// {
// name: "Francisco",
// subscribe: "on", // The default when no "value" is provided
// terms: "accepted",
// gender: "male"
// }
It prevents the default action automatically. See the tests for more examples of how the fields are parsed.
onError
Optional prop to handle any error happening in the onSubmit
. This allows the onSubmit to fail as desired. Works well with both sync and async onSubmit
:
import Form from "form-mate";
export default () => {
const [error, setError] = useState();
const onSubmit = (data) => {
throw new Error("Aaaaagh");
};
return (
<Form onSubmit={onSubmit} onError={setError} autoReset>
{error ? <p>{error.message}</p> : null}
<input name="fullname" required />
<input name="email" type="email" required />
<button>Subscribe!</button>
</Form>
);
};
However for very simple error management like the above, it would be better served with <FormError />
:
<Form onSubmit={onSubmit} onError={setError} autoReset>
<FormError>{(msg) => (msg ? <p>{msg}</p> : "")}</FormError>
<input name="fullname" required />
<input name="email" type="email" required />
<button>Subscribe!</button>
</Form>
onChange
Listen to the forms' changes in fields as they happen. It will be triggered on every keystroke, on every click (that changes the data), etc:
import Form from "form-mate";
export default function Subscribe() {
return (
<Form onChange={(data) => console.log(data)}>
<input name="name" defaultValue="Francisco" />
<input name="subscribe" defaultChecked type="checkbox" />
<input name="terms" value="accepted" defaultChecked type="checkbox" />
<input name="gender" value="female" type="radio" />
<input name="gender" value="male" defaultChecked type="radio" />
<button>Subscribe!</button>
</Form>
);
}
It can be useful for e.g. validate data, or to do a preview-like with the content of the fields.
autoReset
By default the form is not reset after it's submitted. This prop can make the form to reset after the onSubmit callback has resolved successfully:
<Form onSubmit={...} autoReset>...</Form>
Even with this prop, the form will not be reset if the onSubmit
throws an error (sync or async).
This prop is very useful when adding new items to a list in succession, see codesandbox example.
encType
The encType can be set to multipart/form-data
to upload files:
import Form from 'form-mate';
export default() => (
<Form onSubmit={...} encType="multipart/form-data">
<input name="name" />
<input type="file" name="file" />
<button>Send</button>
</Form>
);
In that case, the argument data
passed to the onSubmit will not be a plain object, it will be an instance of FormData
instead.
<FormError />
When there's an error on the onSubmit
function, you can use this component to display it in a variety of ways:
import Form, { FormError } from 'form-mate';
export default () => (
<Form onSubmit={...}>
// A plain string with any error message from onSubmit
<FormError />
// A simple message that is displayed only when there's an error
<FormError>There was an issue...</FormError>
// Use the actual error message within a callback
<FormError>{msg => msg ? <p className="error">{msg}</p> : ''}</FormError>
<input name="name" />
<button>Send</button>
</Form>
);
<FormError />
will display the.message
property of the error, only when an error was thrown.<FormError>Hello</FormError>
will display the "Hello" message only when there's an error. This is useful for e.g. complimentary error icons, or messages, that are not the main thing.<FormError>{msg => msg ? 'a' : 'b'}</FormError>
this will always call the callback; if there was an error, its.message
will be the first argument, and if there was no error it will be empty.
<FormLoading />
A component used to handle loading state of the form. The form starts "loading" when its submitted, and finishes loading when the onSubmit() callback finishes executing (since that function is normally async):
import Form, { FormLoading } from 'form-mate';
export default () => (
<Form onSubmit={...}>
<FormLoading>Loading...</FormLoading> // Renders only while loading
<input name="name" />
<button>
// Is always executed, receiving the loading status as the first param
<FormLoading>
{loading => loading ? 'Sending...' : 'Send'}
</FormLoading>
</button>
</Form>
);
<FormLoading>Hello</FormLoading>
will display the "Hello" message only while the form is loading. This is useful for e.g. complimentary messages, loading indicators, etc.<FormLoading>{loading => loading ? 'a' : 'b'}</FormLoading>
this will always call the callback; if the component is loading it will receivetrue
as it's only parameter, if it's not then it'll receivefalse
.
Examples
Add items to a list
A fully working shopping list example (see codesandbox):
import React, { useState } from "react";
import Form from "./Form";
export default function Groceries() {
const [items, setItems] = useState([]);
return (
<ul>
{items.map((item) => (
<li key={item.text}>
{item.text} × {item.quantity}
</li>
))}
<li>
<Form onSubmit={(item) => setItems([...items, item])} autoReset>
<input name="text" placeholder="Item to buy" autoFocus />
{" × "}
<input type="number" name="quantity" defaultValue={1} />
<button>Add!</button>
</Form>
</li>
</ul>
);
}
Upload files with React
To upload files with React and Axios, you can do it like this:
import Form from 'form-mate';
export default function App() {
const onSubmit = data => {
// Send the data to the server
await axios.post("/hello", data, { "Content-Type": "multipart/form-data" });
};
return (
<Form onSubmit={onSubmit} encType="multipart/form-data">
<input name="name" />
<input type="file" name="file" />
<button>Send</button>
</Form>
);
}
Axios and Hot-Toast
The common pattern to handle forms is to submit the data with Axios, so let's do so:
// This should be somewhere in your codebase, let's assume that your
// API returns a body with { error: "message" } when appropriate
import axios from "axios";
axios.interceptors.response.use(
(res) => res.data,
(error) => {
// A better message to show to the user than the default HTTP one
const message = error.response?.data?.error;
if (message) {
logError(message); // Or any other preferred way
throw new Error(message);
}
// No message; just re-throw the previous error (or anything else)
throw error;
}
);
Then when handling our form, we just need to add an onSubmit as usual, without try/catch, since it'll already be caught by Form Mate and handed with the onError
:
import Form from "form-mate";
import axios from "axios";
import toast from "react-hot-toast";
export default function LoginForm() {
const redirect = useRedirect(); // With your favourite routing lib
const onSubmit = async (data) => {
await axios.post("/login", data);
toast.success(`Successfully logged in!`);
redirect("/");
};
// Show the error, wherever it comes from, on a popup
const onError = (error) => toast.error(error.message);
return (
<Form onSubmit={onSubmit} onError={onError}>
...
</Form>
);
}
Conditional fields
Let's say you want a bit of context of where your users are from. So you ask them for their country, and if the country they provide is the USA you also want their state. Otherwise you don't care if in Spain they are from Madrid or Barcelona.
So we want to render the field to select the state only when the user is from the USA, let's see one way of doing it. We create a form-level variable called "showState", which will be set to true
when in the USA and to false otherwise, and hence render the state selector when set to true.
export default function RegisterForm() {
// When in the USA, `showState` should be set to `true`. The select
// Defaults to the first country, which is the USA
const [showState, setShowState] = useState(true);
const onChangeCountry = (e) => {
const newCountry = e.target.value;
// If the new value is the same as the old one, React will
// ignore this, so we don't need to manually check with an if()
setShowState(newCountry === "usa");
};
return (
<Form onSubmit={onSubmit}>
<select name="country" onChange={onChangeCountry}>
<option value="usa">USA</option>
<option value="esp">Spain</option>
<option value="jpn">Japan</option>
...
</select>
{showState ? (
<select name="state">
<option value="ca">California</option>
<option value="ma">Massachusetts</option>
<option value="ny">New York</option>
...
</select>
) : null}
<br />
<button>Send</button>
</Form>
);
}
There are few other ways of doing this, like with <Form onChange={...}>
, or with a ref
assigned to the country selector.
If we want to apply styles ot our state selector and not have it jump in but instead transition from opaque to visible, we could always render it but hidden it; for that we'd also remove the name
when hidden to avoid it being present when submitting:
<select
name={showState ? "state" : null}
style={{
transition: "all .2s ease",
opacity: showState ? 1 : 0,
pointerEvents: showState ? "all" : "none",
}}
>
<option value="ca">California</option>
<option value="ma">Massachusetts</option>
<option value="ny">New York</option>
...
</select>
HTML Validation
While React Form is not focused on validation, you can still perform three kind of validations: HTML validation, Soft Validation and Hard Validation. We only recommend HTML Validation with this library, but you can do the others in a pinch.
What do we mean by HTML validation? What your browser provides, you can add fields like required
, specify the type="email"
and such for proper formatting:
required
: the field needs to be filled for it to be validtype="number|email|phone|..."
: the field needs to be formatted like an {number|email|phone|...} to be validminlength="3"
andmaxlength="10"
: the field needs to have that number of characters (for strings).min="3"
andmax="10"
: the field needs to be between those values (for numbers).pattern
: the data needs to match a regular expression.
As always, MDN documentation on Built-in validation is spectacular, so please refer to it for further information.
A basic example for a LoginForm would be:
export default function LoginForm() {
return (
<Form onSubmit={console.log}>
<div>
<input name="email" type="email" required />
</div>
<div>
<input name="password" type="password" required minlength="10" />
</div>
<button>Login</button>
</Form>
);
}