Originally published on my blog.
Introduction
There have been interesting (and opposing) ideas of how form state should be handled in React. Some lean towards keeping the form state in a globally (like in Redux, MobX etc;), some prefer keeping it locally, some prefer to render forms with a schema etc.
Some of the popular libraries for form handling in React
Why forms state should be local ?
I could be hitting the hornet's nest with this, but I believe form state should be kept locally in the component and not in the global state container. The primary reason for this argument is because if we reuse the same form component elsewhere in our app, we often want different state for both the forms. Sure, we could create additional pieces state for each instance of the form component, but this defeats the purpose of the global state which is to share same state across different components.
Handling form state locally
Lets start by handling the input state change using Hooks.
// ....
const [firstName, setFirstName] = React.useState('');
const handleFirstNameChange = ({ target: value }) => setFirstName(value);
// ....
<input
type='text'
name='firstname'
value={firstname}
onChange={handleFirstNameChange}
/>;
// ....
Now lets add validation and error message.
// ....
const [firstName, setFirstName] = React.useState('');
const [firstNameError, setFirstNameError] = React.useState('');
const handleFirstNameChange = ({ target: { value } }) => {
if (value.match(/^[a-zA-Z]*$/)) {
firstNameError('');
} else {
firstNameError('Field firstname is not valid !');
}
setFirstName(value);
};
// ....
<input
type='text'
name='firstname'
value={firstname}
onChange={handleFirstNameChange}
/>;
{
firstNameError && <span>{firstNameError}</span>;
}
// ....
Looking pretty good, but imagine doing this for 5 input fields in a form, across 5 different forms in our app. If we decide to copy the same code over, we are bloating the codebase, and the headache would kick in if try to debug or extend the form.
Can we do better ?
Lets start by creating a custom hook and tracking the input change.
// ...
const useForm = () => {
const [values, setValues] = React.useState({});
const onChangeField = ({
target: { name, value }
}: React.ChangeEvent<HTMLInputElement>) => {
setValues(prevState => ({ ...prevState, name: value }));
};
return { values, onChangeField };
};
// ...
const { values, onChangeField } = useForm();
<input
type='text'
name='firstname'
value={values.firstname}
onChange={onChangeField}
/>;
// ...
Now, lets add the initial field state.
// ...
const useForm = (props) => {
const { initialState } = props;
const [values, setValues] = React.useState(initialState || {});
const onChangeField = ({
target: { name, value }
} => {
setValues(prevState => ({ ...prevState, [name]: value }));
};
return { values, onChangeField };
};
// ...
const {values, onChangeField} = useForm({initialState: {
firstname: 'John'
}})
<input type='text' name='firstname' onChange={onChangeField} value={values.firstname} />;
// ...
The key point here is that we use the name
of each field as the key
for the different pieces of state we create. So for example error.firstName
will contain the error of the firstName
field and touched.firstName
will contain the touched state of firstName
and so on.
Now lets throw in some validation and the form submit handler.
// ...
const useForm = props => {
const [values, setValues] = React.useState(props.initialState || {});
const [errors, setErrors] = React.useState({});
const isFieldValid = (name: string, value: string) => {
if (props.validator[name]) {
return !!value.match(props.validator[name]);
}
return true;
};
const onChangeField = ({
target: { name, value }
}: React.ChangeEvent<HTMLInputElement>) => {
if (!isFieldValid(name, value)) {
setErrors(prevErrors => ({
...prevErrors,
[name]: `Field '${name}' not valid !`
}));
} else {
setErrors(prevErrors => ({ ...prevErrors, [name]: null }));
}
setValues(prevState => ({ ...prevState, [name]: value }));
};
const onSubmit = () => {
if (props.onSubmit === "function") {
props.onSubmit(values);
}
};
return { values, onChangeField, errors, onSubmit };
}
// ...
const { onChangeField, values, errors, onSubmit } = useForm({
initialState: { firstname: 'John' },
validator: { firstname: /^[a-zA-Z]*$/ }
onSubmit: vals => console.log(vals)
});
// ...
<form onSubmit={onSubmit}>
<div>
<label>FirstName</label>
<input
type='text'
name='firstname'
onChange={onChangeField}
value={values.firstname}
/>
{errors.firstname && <span>{errors.firstname}</span>}
</div>
</form>
};
We've now built a truly portable hook that can handle forms in our app. We could keep going and add touched state, handle blur, field mount state, form submit state etc.
Source code
Checkout the full source at CodeSandbox
Conclusion
Using plain React could lead to making our components more readable and very maintainable. You can extend this hook and use across your app.
If you need a more mature library built with the same philosophy, checkout Formik. It has a fully fledged API with support for focus management, touched state, handling blur, support for React Native and more. It is one of most versatile form library out there !
Reference
- Formik (try reading the source, it's beautiful ✨)
Top comments (0)