Learning Form Management in React
By building our own reusable component from first principles
Learning often comes from deliberate practice and repetition—making the same mistakes, writing the same code, solving the same problems over and over again.
For example, how many times have you had to:
- Render a form with some inputs
- Collect data from these inputs as the user enters values
- Process the collected data on submit
Me personally — multiple times, in every project.
🤔 The Simple Solution
Of course there already are libraries for managing forms in React. A couple of the most popular are:
- Jared Palmer’s Formik, which uses the same principles described in this post
- redux-form, which manages form state via Redux (well, duh)
If you’re in a real hurry to put some forms in your project I encourage you to stop reading right now and use one of those libraries — they’re full-featured, well-tested, and have a significant community around them.
Otherwise, I’ve found that to understand a tool in more depth it really helps to try and build your own and then switch to a library if you really need to.
In fact, I only started appreciating the benefits and trade-offs of Redux when I went through the process of writing a state management library myself.
We’ll be doing something similar here — by writing a reusable <Form>
component from first principles, we can learn a thing or two about why libraries are built the way they are, as well as how to use them more effectively.
Let’s go.
🍰 The Delicious Demo
TL;DR — here’s what we’ll be building:
Now I’m sure you could find your way around the code just fine — still, as part of the learning experience, I’ll go over the process in more detail.
1️⃣ The Boring Beginning
To figure out what problems we’re trying to solve, let’s first write a standard React form from scratch:
class App extends React.Component {
state = {}; submit = (e) => {
e.preventDefault(); // Prevent submitting form to the server window.alert(`Hey, ${this.state.name}! You ordered some ${this.state.food}!${this.state.isDessert ? ' A lovely dessert!' : ''}`);
}; setName = (e) => this.setState({ name: e.target.value });
setFood = (e) => this.setState({ food: e.target.value });
setIsDessert = (e) => this.setState({ isDessert: e.target.checked }); render() {
return (
<form onSubmit={this.submit}>
<label>
What's your name?
<input type="text" onChange={this.setName} required autoFocus />
</label> <label>
What's your favorite food?
<input type="text" onChange={this.setFood} required />
</label> <label>
<input type="checkbox" onChange={this.setIsDessert} />
It's a dessert
</label> <button type="submit">Order</button>
</form>
);
}
}
This does exactly the same thing as the demo from the previous section — it renders a form with:
- Two text inputs
- One checkbox
- And a submit button.
When the form is submitted it shows an alert dialog that “orders” your favorite food.
2️⃣ The Refactoring Round
One painfully dull thing was that we wrote three methods for collecting values from each input. So if we had 300 inputs would we then have to write 300 methods?
Let’s change that by using a more generic method instead:
class App extends React.Component {
... setValue = (e) => {
const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value;
this.setState({ [e.target.name]: value });
};
render() {
return (
<form onSubmit={this.submit}>
<label>
What's your name?
<input type="text" name="name" onChange={this.setValue} required autoFocus />
</label> <label>
What's your favorite food?
<input type="text" name="food" onChange={this.setValue} required />
</label> <label>
<input type="checkbox" name="isDessert" onChange={this.setValue} />
It's a dessert
</label> <button type="submit">Order</button>
</form>
);
}
}
The main things here are that we:
- Added a
name
attribute to inputs - And handled checkboxes a bit differently from text inputs by using the target’s
checked
attribute instead ofvalue
.
Now, whether we have 3 or 300 components in there, we only need to set a name
attribute and that one onChange
handler.
3️⃣ The Rad Rendering
To make this component’s behavior reusable across our application, we need to be able to provide different inputs as its children, as well as attach onChange
handlers from the parent component to said children.
And how can we both:
- Provide children to a parent component
- And allow children to access functions from their parent?
Why, by using the rad render props pattern of course!
class Form extends React.Component {
state = {}; submit = (e) => {
e.preventDefault();
this.props.onSubmit(this.state);
}; setValue = (e) => {
const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value;
this.setState({ [e.target.name]: value });
}; render({ children, onSubmit, ...props } = this.props) {
return (
<form onSubmit={this.submit} {...props}>
{children({ setValue: this.setValue })}
</form>
);
}
}
The trick here is that the children
props of our component is actually a function — one that we call with an object exposing some API from the <Form>
component to its children — in this case just the setValue
function.
We also automatically prevent the form’s default behavior of submitting to the server and call the provided onSubmit
prop with the collected form values instead.
4️⃣ The Complete Code
Our <Form>
component is now reusable across our entire application, and even across many different React applications if you think about it!
So let’s use it in our <App>
component:
class App extends React.Component {
submit = (values) => {
window.alert(`Hey, ${values.name}! You ordered some ${values.food}!${values.isDessert ? ' A lovely dessert!' : ''}`);
} render() {
return (
<Form onSubmit={this.submit}>
{({ setValue }) => (
<fieldset>
<label>
What's your name?
<input type="text" name="name" onChange={setValue} required autoFocus />
</label> <label>
What's your favorite food?
<input type="text" name="food" onChange={setValue} required />
</label> <label>
<input type="checkbox" name="isDessert" onChange={setValue} />
It's a dessert
</label> <button type="submit">Order</button>
</fieldset>
)}
</Form>
);
}
}
We wrapped our inputs with the <Form>
component and converted the children into a function returning children which exposes the setValue
method from the <Form>
component.
Notice that to avoid listing children as an array we wrapped them in an extra element, more specifically — a fieldset. One cool thing about the fieldset element is that disabling it also automatically disables all of its child inputs too!
💯 Bonus
We now have a very basic <Form>
component we can use in our React application. As a thought exercise, consider some additional ways we could enhance that component:
- Allow setting the initial values and make child inputs fully controlled
- Define children as a function in
propTypes
in order to have a more robust<Form>
component that throws an error whenchildren
isn’t a function - If you want to make
<Form>
aReact.PureComponent
to improve performance, you might also want to look into the implications of using render props with pure components - If the form is more complex, consider using dot notation in the input’s
name
attribute to handle deeply nested data — for example the component<input type="text" name="name.first" value="Albert" />
would return the object{ name: { first: 'Albert' } }
- Incorporate data validation and error messages in your form by utilizing the
e.target.validationMessage
property and other HTML5 Form Validation features - Try using JSON schema to both generate form inputs and validate them — this also allows you to share data validation rules with other environments, for example a backend server.
🏁 Conclusion
Like I said at the beginning, learning often comes through deliberate practice and repetition. And after going through the process of building a simple <Form>
component myself, I think I understand the trade-offs well enough to finally switch to a library like Formik.
Hope this leaves you too with plenty of enthusiasm for forms — have fun building!