Forms, like death and taxes, are one of the unavoidable aspects of web development. From Login Forms, Contact Pages, to heavy form-centric applications - at some point, you will need to wire up an input field to your application state. Working with forms in React can require a bit of boilerplate, and while there are libraries like Formik, Redux Form or React Redux Form can help make things easier, they can be overkill for many situations.
React Hooks were announced at React Conf 2018, and are on the roadmap for release in early 2019. Hooks provide a way to handle stateful logic in functional components, while also providing a way to share non-UI logic and behaviour across your application. To better understand this, and see it in action, I will be walking you through how to leverage the useState hook for simplifying a controlled component, and then creating a custom hook to simplify input handling even more.
Stay up to date on what's happening in the digital transformation space by signing up for our newsletter, here.
Stay up to date with the latest featured content in our Front End Hub!
Controlling Inputs The Old Way
First, let's start with an example from the React Docs on Forms, and create a controlled component:
class NameForm extends React.Component {
constructor(props) {
super(props);
this.state = {value: ''};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(event) {
this.setState({value: event.target.value});
}
handleSubmit(event) {
alert('A name was submitted: ' + this.state.value);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Name:
<input type="text" value={this.state.value} onChange={this.handleChange} />
</label>
<input type="submit" value="Submit" />
</form>
);
}
}
While this code is not very complex, it does feel a little heavy handed for handling a single input field.
Breaking down what is going on here, we want to:
- Keep track of state for an input field
- Update the value on change
- Make the field value available to the submit handler
Can we rewrite this NameForm as a functional component using hooks?
Getting Functional
Let's start by creating a functional component for the NameForm:
import React, { useState } from "react";
export function NameForm(props) {
return (
<>
<label>
Name:
<input type="text" />
</label>
<input type="submit" value="Submit" />
</>
);
}
The input field has state we need to manage, this can be done using the useState hook.
useState will return an array, with the first item in it being the current value of the state, and the second item being a setter to update that value.
Wiring up the form submit handler is now trivial also:
import React, { useState } from "react";
export function NameForm(props) {
const [name, setName] = useState("");
const handleSubmit = (evt) => {
evt.preventDefault();
alert(`Submitting Name ${name}`)
}
return (
<form onSubmit={handleSubmit}>
<label>
Frirst Name:
<input
type="text"
value={name}
onChange={e => setName(e.target.value)}
/>
</label>
<input type="submit" value="Submit" />
</form>
);
}
This is a good start and has already cleaned up handling an input. We don't need to create an instance of the React component class, worry about the context of this and we can start to get the benefits of functional components while still being able to do stateful logic.
We are not limited to the hooks that React provides, but can also build your own hooks, and this is where the value of hooks starts to shine for me:
The ability to have reusable logic shared across components, without having to introduce extra components into our hierarchy.
There are many patterns out there that can accomplish this such as Higher Order Components, Render Props, etc. These patterns are still useful, however, if you have a HoC, or component using render-props that has no UI to it and is only rendering a single child - a hook might be a better way of handling this.
Creating Custom Hooks
To see what this looks like, I will create a useInput hook to simplify handling inputs even further.
import { useState } from "react";
export const useInput = initialValue => {
const [value, setValue] = useState(initialValue);
return {
value,
setValue,
reset: () => setValue(""),
bind: {
value,
onChange: event => {
setValue(event.target.value);
}
}
};
};
We can then update our NameForm to:
import React from "react";
import { useInput } from './hooks/input-hook';
export function NameForm(props) {
const { value, bind, reset } = useInput('');
const handleSubmit = (evt) => {
evt.preventDefault();
alert(`Submitting Name ${value}`);
reset();
}
return (
<form onSubmit={handleSubmit}>
<label>
Name:
<input type="text" {...bind} />
</label>
<input type="submit" value="Submit" />
</form>
);
}
If we wanted to add more fields, we could rename the values in our destructuring like so:
export function NameForm(props) {
const { value:firstName, bind:bindFirstName, reset:resetFirstName } = useInput('');
const { value:lastName, bind:bindLastName, reset:resetLastName } = useInput('');
const handleSubmit = (evt) => {
evt.preventDefault();
alert(`Submitting Name ${firstName} ${lastName}`);
resetFirstName();
resetLastName();
}
return (
<form onSubmit={handleSubmit}>
<label>
First Name:
<input type="text" {...bindFirstName} />
</label>
<label>
Last Name:
<input type="text" {...bindLastName} />
</label>
<input type="submit" value="Submit" />
</form>
);
}
With hooks on the React Roadmap to land in Q1 of 2019, I'm excited to see how the leveraging them can start cleaning up and simplifying the code I am working on today. If you're interested in learning more about how to start applying it in your own code, stayed tuned for my upcoming post on how to refactor your existing components to leverage the power of hooks.