Why do we have to talk about form state, again? 🥴

Guillermo Polit
5 min readJul 11, 2021

--

So here we go again, form state… we have to reinvent the wheel each time we need to build a simple form.

🙃 What is the real problem here?

Each library has its own definition of how form state needs to be managed and doing it with plain a HTML5 form is a little bit of a pain in the %^&*(.

I will talk in particular about React, but pretty much this same concept applies to the majority of libraries out there. The abstraction created between the form and the state is too big to understand. The documentation ends up being a huge monster.

💡 Why is this?

The main point here is that all the libraries out there are trying to do everything for you. This ends up adding a hell of a lot of complexity, mixing UI and state a lot, and making really hard to maintain and change in the long term.

We have to add to this mix the problem that the requirements on forms are so vast and different that it is extremely hard to please everyone. Obviously, there are some concepts that everyone needs to cover in all possible ways, so there is where I’m going to be starting.

Before jumping into some detail, there are 2 main libraries out there that are the most used for React.

  • good old Formik
  • the hook-like react-hook-form

Unfortunately, they both have the same problem, for being able to actually be proficient in building a form more than one time in your life you need to be a Javascript code guru and have an eidetic memory to remember the documentation. So please stay with me!

❓Why do we need to cover everything in one library?

So this is what React taught us. You need to be good at only what you do and trying to be a Jedi in everything makes you feel like you are building the Angular framework yourself.

I think that the main idea here is the old concept of divide and conquer.

☢️ How to conquer Westeros by conceptually dividing form state?

  1. Data validation.
  2. Form view.
  3. State management.

1️⃣ Data validation

There are so many libraries out there that do this in an excellent way. Some are being used for many long years. Some others are the new rookies that are showing the old ones how things should be done. And there are libraries out there that are not available yet, that will rock the development world.

2️⃣ Form View

There are infinite libraries, design systems, and stand-alone components that we could use for building a form UI. So our library needs to be able to adjust to all of them! That is the main idea here. Also, we should not need to be told how to build the UI, we should just care about how we need to display it and not the other way round.

3️⃣ State management

This is what we need to care about. If data validation can be any, and the form view follows the same. The one that drives both needs to be the state. The state is what drives any application, and what makes the user experience change.

✅ State management to the rescue.

This is why I ended up building a library for managing just the state, that can adjust to any data validation library and any form UI.

My main idea was to bring any data validator library, bring any UI you have, and make something that can mix them both without adding complexity and dependencies.

I added some more sugar to this mix by allowing you to hook it to redux, using it as a standalone react hook (no need of redux), or using it under a Container/Provider pattern.

Github repo here

So far I have created 2 adapters for data validation, one for yup and one for jsonSchema. I decided to make it as peerDependencies just for simplifying things and easy adoption (you choose the one you want to depends on, just install that). You can bring your own library easily if you want too.

For UI, it doesn't matter. The important thing in this library is the hook output state shape. It automatically mixes the initialState of your form with the initial schema validation and transforms this into a readable state you can hook to anything.

The state is quite easy to read, I tried to build the library as declarative as possible so it is easy to use without having to check the documentation all the time.

State shape example

isSubmitted: false,
isSubmittable: false,
isTouched: true,
isPristine: false,
isValid: false,
isInvalid: true,
fields: {
familyName: {
value: 'Doe',
isPristine: true,
isTouched: false,
isSubmitted: false,
isInitialized: true,
isValid: true,
isInvalid: false,
showError: false,
path: 'profileOne.familyName',
},
alias: {
value: '',
isPristine: false,
isTouched: true,
isSubmitted: false,
isInitialized: true,
error: 'alias is a required field',
isValid: false,
isInvalid: true,
showError: true,
path: 'profileOne.alias',
},
...
}

You can even nest forms if you want, no issue.

How to?

const YUPSchema = YUP.object().shape({
alias: YUP.string().required(),
});
const MyForm = ({ onSubmit }) => {
const [formState, { updateField, submitForm }] = useMyFormState({
initialState: { alias: 'guiyep' },
formSchema: yup.formSchema(YUPSchema),
});
const onFieldChangeHandler = (field, value) => updateField({ field, value });const onSubmitHandler = async () => {
const result = await submitForm();
onSubmit(result);
};
return <Form formState={formState} onFieldChange={onFieldChangeHandler} onSubmit={onSubmitHandler} />;
};
export default MyForm;

Finally… UI, I chose to use Material UI but you can easily adjust it to any UI library/design system/stand-alone component.

import React, { useCallback } from 'react';
import TextField from '@material-ui/core/TextField';
import Button from '@material-ui/core/Button';
const Form = ({
formState: {
fields: { name },
isSubmittable,
isSubmitted,
isInitialized,
},
onFieldChange,
onSubmit,
}) => {
const onFieldChangeHandler = useCallback((e) => onFieldChange(e.target.id, e.target.value), [onFieldChange]);
return (
<form noValidate autoComplete="off">
<div>
<TextField
error={name.showError}
required
id={name.path}
label="Name"
value={name.value}
margin="normal"
onChange={onFieldChangeHandler}
disabled={isSubmitted}
/>
</div>
<div>
<Button disabled={!isSubmittable || isSubmitted} onClick={onSubmit}>
Submit
</Button>
</div>
</form>
);
};
export default Form;

😍 Easy-as!

The Form component can be created with any UI. We use data schemas for data validation. In this case, we use YUP. Please look in the documentation at how to use JSON schema. You can even bring your own library, no issue.

Conclusion

Divide and conquer, that is what we are doing here. We need a way of splitting concerns without modifying the way we work.

🙌 Welcome to the joy of my-react-form-state 🙌

Here is the 🔍 documentation if you want to explore more.

Thanks for your time! and shoot me a message if you have any questions!

--

--

Guillermo Polit

I'm an enthusiastic engineer that love to share and spread knowledge.