Forms are fun. They start simple then blow up in your face.
You've seen this pattern in every React tutorial with form fields:
const [firstName, setFirstName] = useState("")
// ...
;<input
name="firstName"
value={firstName}
onChange={(event) => setFirstName(event.target.value)}
/>
A controlled input. Gets value from state, updates value on change. Works great 👍
Keeping form values in state is convenient for business logic. You trigger a re-render for every keypress but that's okay, React is fast.
Then you add more fields ...
const [firstName, setFirstName] = useState("")
const [lastName, setLastName] = useState("")
const [address1, setAddress1] = useState("")
const [address2, setAddress2] = useState("")
const [city, setCity] = useState("")
const [zip, setZip] = useState("")
const [state, setState] = useState("")
That sure blew up 😅
State up the wazoo, re-renders on any field change, and you haven't even thought about validation, error states, dirty states, and all the rest that goes into a form.
You're smart and you can make this work. I know you can .
But imagine a monster form like I was dealing with last week.
twitter: pffft you don't need a library for form state
— Swizec Teller (@Swizec) July 30, 2020
my validation logic: you wot m8pic.twitter.com/bIjc4WaBiW
😇
Custom form generators help
You can build a form generator to remove the repetitive work.
Helper component, a loop or two, and off you go, right?
const Input = ({ name, value, setValue, placeholder }) => (
<input
name={name}
value={value}
placeholder={placeholder}
onChange={(event) => setValue(event.target.value)}
/>
)
Ok I didn't use loops, but you get the idea.
The <Input>
component is generic and we pass-in state and setters from above. Then you can add errors, default values, different styles, etc.
You don't want to push state handling into the Input
component itself. That's a recipe for pain.
A better approach might be to turn form state into an object and build a reducer.
const [
{ firstName, lastName, address1, address2, city, zip, state },
setState,
] = useState({
firstName: "",
lastName: "",
address1: "",
address2: "",
city: "",
zip: "",
state: "",
})
function valueSetter(name) {
return (value) =>
setState((formState) => {
return {
...formState,
[name]: value,
}
})
}
// ...
;<Input name="lastName" value={lastName} setValue={valueSetter("lastName")} />
Your form state lives inside an object – one key per field. You can always see current state and you always know what's going on.
For easy updates you've got the valueSetter
method that returns a function to update a single field.
Works great 👍
You're triggering a full re-render for every field change, you don't have validations, no way to show errors, and no way to know when a field is dirty.
PS: dirty fields are fields that changed and the value hasn't been saved yet
react-hook-form makes everything better
react-hook-form solves those problems for you.
It's a hook-based form solution that takes care of:
- managing state
- errors
- dirty fields
- validations
And minimizes re-renders by avoiding controlled inputs.
Yep, react-hook-form uses the fields themselves to keep state. Pulls it out when you need it.
Here's the example above built with react-hook-form:
State management turns into this:
const { register, handleSubmit } = useForm()
Setting up a field turns into this:
const Input = ({ name, register }) => <input name={name} ref={register} />
The register
method returns a React ref and sets up the form machinery.
And when you're ready to submit, values come as an object with a key for every field.
You get HTML5 validations out of the box. They show up in the errors
object. Like this:
const Input = ({ name, register, errors, placeholder }) => (
<>
<input
name={name}
ref={register({ minLength: 5 })}
placeholder={placeholder}
/>
{errors && <span>{name} is invalid</span>}
</>
)
// ...
const { register, handleSubmit, errors } = useForm()
// ...
;<Input name="firstName" register={register} errors={errors.firstName} />
By default validations run on change.
And you can change that with a line of code 😍
const { register, handleSubmit, errors } = useForm({
mode: "onBlur",
})
Add context for more flexibility
Passing errors and registers into every field like above is silly. Too much work.
That's why react-hook-form supports context.
Using the same approach as my Wormhole state management article:
- create a form
- put everything in context
- wormhole to context with a hook
You get form components that magically connect to your form. Register themselves, know about errors, dirty states. Everything 😍
Like this
const Input = ({ name, placeholder }) => {
const { register, errors } = useFormContext()
return (
<>
<input
name={name}
ref={register({ minLength: 2 })}
placeholder={placeholder}
/>
{errors[name] && <span>{name} is invalid</span>}
</>
)
}
Every field validates a 2 character minLength
and displays its own errors.
Using them in a form looks easy now:
<Input name="firstName" />
Add Yup for big validations
You've got the machinery to spit out forms now. Small schlep and you're done.
Render a form, pepper it with <Input name="X" />
fields and voila. Happy boss, happy customer, happy you.
And then it's time for real validations. Not minLength
or required
. Real validations. The kind where lastName
is required, but only if firstName
is filled in.
You can add yup for that. react-hook-form supports it out of the box ✌️
Here's the example above that validates you wrote the whole address, but only if you wrote the street name.
To make that happen you 👇
Write a Yup schema describing the shape of your data. Bit of an art to it, make sure you don't fall off the deep end with conditional validations. They get tricky.
const schema = yup.object().shape({
firstName: yup.string(),
lastName: yup.string(),
address1: yup.string(),
address2: yup.string(),
city: yup.string().when("address1", {
is: (address1) => address1.length > 0,
then: yup.string().required(),
}),
state: yup.string().when("address1", {
is: (address1) => address1.length > 0,
then: yup.string().required(),
}),
zip: yup.string().when("address1", {
is: (address1) => address1.length > 0,
then: yup.string().required(),
}),
})
The Yup docs are okay, not stellar. I recommend tinkering until it works.
Then you tell react-hook-form about your schema:
const formMethods = useForm({
mode: "onChange",
resolver: yupResolver(schema),
})
And change your register
call back to vanilla:
<input name={name} ref={register} placeholder={placeholder} />
And you've got reusable field components that work with any form. Pop them in, set the schema, get validations and error handling and all the rest.
Happy hacking
Cheers,
~Swizec
PS: in my code I went as far as a generic <Form>
component that sets up the context provider and <form>
element for me.
Continue reading about Why react-hook-form is my new favorite form library
Semantically similar articles hand-picked by GPT-4
- Why you should build a form generator
- Delightful state management with hooks and Constate
- Update state during render, better explained
- How to populate react-redux-form with dynamic default values
- How to use React Context effectively
Learned something new? Want to become a React expert?
Learning from tutorials is easy. You follow some steps, learn a smol lesson, and feel like you got dis 💪
Then comes the interview, a real world problem, or a question from the boss. Your mind goes blank. Shit, how does this work again ...
Happens to everyone. Building is harder than recognizing and the real world is a mess! Nothing fits in neat little boxes you learned about in tutorials.
That's where my emails come in – lessons from experience. Building production software :)
Leave your email and get the React email Series - a series of curated essays on building with React. Borne from experience, seasoned with over 12+ years of hands-on practice building the engineering side of growing SaaS companies.
Get Curated React Essays
Get a series of curated essays on React. Lessons and insights from building software for production. No bullshit.
Have a burning question that you think I can answer? Hit me up on twitter and I'll do my best.
Who am I and who do I help? I'm Swizec Teller and I turn coders into engineers with "Raw and honest from the heart!" writing. No bullshit. Real insights into the career and skills of a modern software engineer.
Want to become a true senior engineer? Take ownership, have autonomy, and be a force multiplier on your team. The Senior Engineer Mindset ebook can help 👉 swizec.com/senior-mindset. These are the shifts in mindset that unlocked my career.
Curious about Serverless and the modern backend? Check out Serverless Handbook, for frontend engineers 👉 ServerlessHandbook.dev
Want to Stop copy pasting D3 examples and create data visualizations of your own? Learn how to build scalable dataviz React components your whole team can understand with React for Data Visualization
Want to get my best emails on JavaScript, React, Serverless, Fullstack Web, or Indie Hacking? Check out swizec.com/collections
Did someone amazing share this letter with you? Wonderful! You can sign up for my weekly letters for software engineers on their path to greatness, here: swizec.com/blog
Want to brush up on your modern JavaScript syntax? Check out my interactive cheatsheet: es6cheatsheet.com
By the way, just in case no one has told you it yet today: I love and appreciate you for who you are ❤️