A few weeks ago, I released Fielder - a form library for React.
That's right, another form library for React competing with the already long-standing leader that is Formik... but hear me out.
This isn't a marketing blog. I'm not going to attempt to brainwash you into using one library over the other. Instead I want to talk about how we currently manage form state, and why I think we needed a change.
Before I begin, I want to give a huge shoutout to all the contributors & maintainers of Formik. It has been the long standing go-to for most (including myself) and has influenced some of the decisions made while creating Fielder.
Forms aren't (always) static
There are a lot of features in Fielder designed to fix issues I had while working with Formik (keep your eyes peeled for a follow up post going into more detail) but the static nature of most popular form libraries I came across was the key reason I felt the need to create Fielder.
When I say "forms aren't static" I'm referring to aspects of a form's state which could change during a user interaction. These include:
1. Fields
Fields in the form's state have the potential to be added, removed and changed.
Most libraries encourage a pattern of statically declaring fields at form construction (e.g. 'initialValues' in Formik).
2. Validation
Just as fields can come and go, so can validation. Changes to form state occur, and validation should have the ability to change, adapt and evolve as necessary.
In formik, while changing of the validation schema isn't easily supported, there are workarounds such as using where
conditions in Yup. The challenge with this however is that it needs to be declared up-front. This can be tricky when managing a form which has many possible states.
3. Validity state
A valid form isn't necessarily a form which is ready to be submitted. Instead, a form can be considered valid if the current state allows for progression.
Progression can be a final submission; but it can also be another action such as moving to the next section/page of the form.
The best example of this is a multi-step form where the user has to click next in order to proceed to the next step. If all the current visible fields pass validation the form is valid and the user should be able to move to the next step. Whether or not the form is in it's final, valid, submission-ready state at that point in time is irrelevant.
Field level declaration
Once you're sold on the need for dynamic and evolving forms, field level declarations start to make a whole lot more sense.
Field level declarations allow for adding, removing and changing fields in isolation without having to worry about the wider form state. This is an alternative to a monolithic configuration where all initial values and validation options are declared up front and high up in the component tree.
Configuring a field
With popular libraries such as Formik, you'll be used to a monolithic form config where form and field initialization occur at the same time:
const formConfig = {
initialValues: {
firstName: 'Carla',
lastName: 'Jones',
},
validation: Yup.object().shape({
firstName: Yup.string(),
lastName: Yup.string(),
}),
validateOnChange: true,
};
const formState = useFormik(formConfig);
With field-level declaration patterns (and therefore Fielder), form initialization is isolated.
const formState = useForm();
Forms always start in the same state - empty. It's the responsibility of fields to add, remove and change their value within the form.
const [firstState, firstMeta] = useField({
initialValue: 'Carla',
validate: useCallback(
(value) => Yup.string().validateSync(value),
[]
),
validateOnChange: true,
});
Working with hooks
Field-level validation ties in really well with React's hooks because the lifecycle of a field corresponds closely to that of a component. Alongside this, because fields can now be declared lower down inside the component tree, we have the ability to access state which is specific to our component.
This allows us to do funky stuff such as this:
const [state, setState] = useState({ isRequired: true });
const [firstState, firstMeta] = useField({
// Initial value conditional on component props
initialValue: props.firstName || 'Carla',
// Validation conditional on component state
// (immediately applies on change)
validate: useCallback(
(value) => {
if (state.isRequired && !value) {
throw Error('First name is required');
}
},
[state.isRequired]
),
// Performance optimizations conditional on component state
// (immediately applies on change)
validateOnChange: state.isDesktop
});
Validation that encourages good UX
The progressive and evolving nature of field-level declaration encourages design patterns which follow a similar pattern.
๐ Regression
The valid state of a field on page 1 is conditional on the value of a field on page 2
This is an example of a bad user experience. After already having moved forward on the form, the user now has to backtrack in order to undo an action and there is no obvious way to show the user where the error has occurred.
๐ Progression
The valid state of a field on page 2 is conditional on the value of a field on page 1
or
A field on page 2 is conditionally presented based on the value of a field on page 1
In these examples, the user is informed about actions they can take currently based on the current state. While the user may be able to go back and change previous values, the current state focuses on what the user can do to move forward with the form.
Enforcing these practices
Regressive validation just straight up isn't possible in Fielder. This is because Fielder does not validate inactive fields (fields which are not mounted).
By only calling validation on mounted fields, we also get the added benefit of a better performing form.
Getting started
If you've read this far, congratulations!
To get to grasp with how all this field-level form theory applies to real world usage, check out some of the live Fielder examples.
Also be sure to check out the repo and the official docs site for more in-depth information and to get started.
Top comments (2)
Formik is nice, but I would be more interested in comparsion with react hook form
Thanks for the comment!
The comparisons to Formik also apply similarly to react-hook-form. This is because both use form-scoped validation as opposed to field-first.
Hope that answers your question!