Photo by rawpixel on Unsplash

Learning Form Management in React

By building our own reusable component from first principles

Vlad Sabev
5 min readOct 8, 2018

--

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:

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 of value.

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 when children isn’t a function
  • If you want to make <Form> a React.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!

--

--