Preface
This is a snippet from my notes as I learn ReactJS for work. If you have any suggestions on how I can improve my code samples, or if you found anything catastrophically wrong, please do not hesitate to let me know!
Contents
- Introduction
- Create a Generic
onChange
Handler - Reset a Form Through an
initialState
- Move State Closer to Forms
- Conclusion
Introduction
So you've learned about what React is and why is it all over the place these days. You've learned what components and props are, as well as how to manage their state and lifecycle. You are also now familiar with the concept of controlled components (i.e. how to manage state through form inputs). In this article, we'll take a look at a few techniques that we can utilize in order to make working with Form Inputs in React more easier.
Note: Examples in this article heavily use ES6 features. If you are not familiar with ES6, Tania Rascia's overview is a good place to start.
Create a Generic onChange
Handler
In order to achieve parity on a state
and <input/>
value (also called two-way data binding), we need to set an <input/>
's value to its corresponding state
and also bind an onChange
handler that computes the new state
value when the <input/>
has been changed. Let's take a look at an example from the ReactJS website (refactored for brevity):
class RegistrationForm extends React.Component {
state = { name: '' }
handleChange = event => this.setState({name: 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.name}
onChange={this.handleChange} />
</label>
<input type="submit"
value="Submit" />
</form>
);
}
}
What this example does is that when the <input/>
's value changes, the state.name
property is also updated. But the state being updated (name
) is hardcoded, which prevents it from being reusable when there are multiple inputs. A solution that I commonly see is to create a handler for each input, which would like this:
state = { name: '', password: '' }
handleNameChange = event => this.setState({name: event.target.value})
handlePasswordChange = event => this.setState({password: event.target.value})
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Name:
<input type="text"
value={this.state.name}
onChange={this.handleNameChange} />
</label>
<label>
Password:
<input type="password"
value={this.state.password}
onChange={this.handlePasswordChange} />
</label>
<input type="submit"
value="Submit" />
</form>
);
}
If we would be working with one or two <input/>
s, this approach would work just fine. But one can imagine when requirements down the road dictates that we need to add more field to this form, then a 1:1 input to handler ratio would quickly become unmantainable. This is where a Generic Handler comes in.
As the name implies, a Generic Handler catches all input events and updates their corresponding state. The key that will be used for the state lookup will be inferred from the name
attribute of an <input/>
. This is what it looks like:
handleChange = event => {
const {name, value} = event.target;
this.setState({ [name]: value });
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Name:
<input type="text"
value={this.state.name}
onChange={this.handleChange} />
</label>
<label>
Password:
<input type="password"
value={this.state.password}
onChange={this.handleChange} />
</label>
<input type="submit"
value="Submit" />
</form>
);
}
Now both <input/>
s only use one handler to update their corresponding state. But what if we need to apply custom logic to specific <input/>
s before updating the state? An example would be to validate if an <input/>
's value is valid, or to apply formatting to specific value. We can do this by checking the name
of the <input/>
and conditionally applying the desired logic:
state = {
name: '',
password: '',
age: null,
}
handleChange = event => {
let {name, value} = event.target;
// Custom validation and transformation for the `age` input
if (name === 'age') {
value = parseInt(value);
if (value < 18) {
alert('Minors are not allowed');
return;
}
}
this.setState({ [name]: value });
}
handleSubmit = event => {
event.preventDefault();
console.log(JSON.stringify(this.state)); // Ready for processing
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Name:
<input type="text"
value={this.state.name}
onChange={this.handleChange} />
</label>
<label>
Password:
<input type="password"
value={this.state.password}
onChange={this.handleChange} />
</label>
<label>
Age:
<input type="number"
value={this.state.age}
onChange={this.handleChange} />
</label>
<input type="submit"
value="Submit" />
</form>
);
}
If the handleChange
method becomes too bloated down the line because of the multiple branches, you might want to consider factoring the complex <input/>
s onto their own component and manage the logic there.
Reset a Form through an initialState
As you might have already experienced, a common process when using an HTML form that creates something is:
- Enter data into the form fields.
- Submit the form.
- Wait for the data to be processed (by an HTTP request to a server, for example).
- Enter data again onto a cleared form.
We already have steps 1 to 3 (if we count the console.log
call as step #3) implemented in the previous example. How could we implement step #4? A perfectly fine (though somewhat naive) solution is to call setState
and pass what the original state
object might contain:
state = {
name: '',
password: '',
age: null,
}
handleSubmit = event => {
event.preventDefault();
console.log(JSON.stringify(this.state)); // Ready for processing
// Reset the state
this.setState({
name: '',
password: '',
age: null,
});
}
Copy and pasting, more often than not, is a good indicator that a better solution is available. What if we add more fields in the future? What if we only want to reset some parts of the form? These could be easily solved by creating an initialState
member on your class:
initialState = {
name: '',
password: '',
age: null,
}
state = { ...this.initialState }
handleSubmit = event => {
event.preventDefault();
console.log(JSON.stringify(this.state)); // Ready for processing
// Reset the state
this.setState({ ...this.initialState });
}
Want to persist the name
when the form is cleared? Simply move it from the initialState
to the state
and it won't get overwritten upon submission:
initialState = {
password: '',
age: null,
}
state = {
name: '',
...this.initialState
}
handleSubmit = event => {
event.preventDefault();
console.log(JSON.stringify(this.state)); // Ready for processing
// Reset the state except for `name`
this.setState({ ...this.initialState });
}
Move State Closer to Forms
With React, it is tempting to move all state as high up the component tree as possible and just pass down props and handlers when necessary.
Functional components are easier to reason with after all. But this could lead to bloated state if we shoehorn everything on the top-level component.
To demonstrate, let's say that the <RegistrationForm/>
component in the previous example is under an <App/>
component in the component tree. <App/>
keeps an array of users in its state and we would like to push the newly registered user from the <RegistrationForm/>
component. Our first instict might be to move state up to the <App/>
component and make <RegistrationForm/>
a functional one:
class App extends React.Component {
state = {
users: [],
newUser: {
name: '',
password: '',
age: null,
},
}
handleChange = e => {
let {name, value} = event.target;
// Custom validation and transformation for the `age` input
if (name === 'age') {
value = parseInt(value);
if (value < 18) {
alert('Minors are not allowed');
return;
}
}
this.setState({ newUser[name]: value });
}
handleSubmit = e => {
e.preventDefault();
const users = this.state.users.slice();
const {name, password, age} = this.state.newUser;
users.push({name, password, age});
this.setState({users});
}
render() {
return <RegistrationForm newUser={this.state.newUser}
handleChange={this.handleChange}
handleSubmit={this.handleSubmit}/>
}
}
const RegistrationForm = ({newUser, handleChange, handleSubmit}) => (
<form onSubmit={handleSubmit}>
<label>
Name:
<input type="text"
value={newUser.name}
onChange={handleChange} />
</label>
<label>
Password:
<input type="password"
value={newUser.password}
onChange={handleChange} />
</label>
<label>
Age:
<input type="number"
value={newUser.age}
onChange={handleChange} />
</label>
<input type="submit"
value="Submit" />
</form>
)
This solution works, and nothing is inherently wrong with it. But let's take a step back and look at it with fresh eyes: does the <App/>
component really care about the newUser
state? Opinions might vary, but heres mine: I think that unless <App/>
manages other components that might need to access it, the newUser
data should be managed solely by who it's concerned with -- <RegistrationForm/>
. The <App/>
component doesn't necessarily care about the low-level details, it just wants a way to add a new user.
Let's do just that!
class App extends React.Component {
state = { users: [] }
addUser = user => {
const users = this.state.users.slice();
users.push(user);
this.setState({ users });
}
render() {
return <RegistrationForm addUser={this.addUser}/>
}
}
class RegistrationForm extends React.Component {
state = {
name: '',
password: '',
age: null,
}
handleChange = e => {
let {name, value} = event.target;
// Custom validation and transformation for the `age` input
if (name === 'age') {
value = parseInt(value);
if (value < 18) {
alert('Minors are not allowed');
return;
}
}
this.setState({ [name]: value });
}
handleSubmit = e => {
e.preventDefault();
this.props.addUser(this.state);
}
render() {
const {name, password, age} = this.state;
return (
<form onSubmit={this.handleSubmit}>
<label>
Name:
<input type="text"
value={name}
onChange={this.handleChange} />
</label>
<label>
Password:
<input type="password"
value={password}
onChange={this.handleChange} />
</label>
<label>
Age:
<input type="number"
value={age}
onChange={this.handleChange} />
</label>
<input type="submit"
value="Submit" />
</form>
);
}
}
See the difference? Now, <App/>
itself doesn't know how the newUser
object is being built. It doesn't have handlers that work with DOM events, which makes sense since it doesn't render any form inputs itself. <RegistrationForm/>
, on the other hand, returns HTML <input/>
s directly, and it only makes sense that it handles input events on its own.
Conclusion
Things to take away from this article:
- A generic
onChange
handler can reduce repeated handler code. - Inferring state from an
initialState
can be useful for resetting a component's state. - Think twice when moving state up the component tree.
- Components that render HTML
<input/>
s directly should be the one with event handlers.
Top comments (3)
How do you feel about an
onChange
handler on the form element, instead of each input?Honestly, I wasn't aware you can do that. It actually makes perfect sense to add the handler on the form itself and put all custom logic on that generic handler. Though I fiddled around and saw that adding another handler on the input itself won't override the handler on the form element, but instead it'll fire first and then run the form's handler. Not 100% sure of the implications here but it's nice to know.
Nice article, but I think using useform for example let things much easy to handle