DEV Community

Cover image for Practical Tips for Handling Forms in React
John Alcher
John Alcher

Posted on • Originally published at johnalcher.me

Practical Tips for Handling Forms in React

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

  1. Introduction
  2. Create a Generic onChange Handler
  3. Reset a Form Through an initialState
  4. Move State Closer to Forms
  5. 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>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
    );
}
Enter fullscreen mode Exit fullscreen mode

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>
    );
}
Enter fullscreen mode Exit fullscreen mode

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>
    );
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. Enter data into the form fields.
  2. Submit the form.
  3. Wait for the data to be processed (by an HTTP request to a server, for example).
  4. 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,
    });
}
Enter fullscreen mode Exit fullscreen mode

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 });
}
Enter fullscreen mode Exit fullscreen mode

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 });
}
Enter fullscreen mode Exit fullscreen mode

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>
)
Enter fullscreen mode Exit fullscreen mode

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>
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. A generic onChange handler can reduce repeated handler code.
  2. Inferring state from an initialState can be useful for resetting a component's state.
  3. Think twice when moving state up the component tree.
  4. Components that render HTML <input/>s directly should be the one with event handlers.

Links and References

Latest comments (3)

Collapse
 
bendman profile image
Ben Duncan

How do you feel about an onChange handler on the form element, instead of each input?

Collapse
 
alchermd profile image
John Alcher

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.

Collapse
 
kamo profile image
KAIDI

Nice article, but I think using useform for example let things much easy to handle