Implementing Controlled Forms with React

Are your forms uncontrollable? Call now!

Controlled forms were one of the first elements I created with React where I just thought, "Wow! That's really cool, and this is something that I couldn't do (easily? at all?) with just vanilla JavaScript." That experience inspired me to write an easy-breezy introduction to creating your own controlled forms with React as quickly as possible. This will discuss the bare essentials of setting up your own controlled form, and can also serve as a jumping off point if you just want to copy the code and try it out. Have fun!

Initial setup

So let's say that you have the bare bones for your React app started: you have your parent container (App), and one of its children is your form component:

//imports
import React from 'react';
import Form from './form';

function App() {
  //App body

  return (
    <div id='app container'>
      <Form />
    </div>
    )
}

export default App;
//imports

function Form() {
  //Form body

  return (
    <form>
      <label for='firstName'>First name</label>
      <input type='text' id='firstName' name='firstName'></input>
      <label for='lastName'>Last name</label>
      <input type='text' id='lastName' name='lastName'></input>
    </form>
    )
}

export default Form;

Step one: setup your state

The first thing you'll want to do is create your 'state'. This will require adding an import for 'useState' from React on your form component. Below you'll see useState being imported, and also our initial addition of state to the Form component body.

//importing useState
import {useState} from 'react';

function Form() {
//Form body
//adding useState to the body
const [formData, setFormData] = useState({
firstName: '',
lastName: ''
});

return (
  <form>
    <label for='firstName'>First name</label>
    <input type='text' id='firstName' name='firstName'></input>
    <label for='lastName'>Last name</label>
    <input type='text' id='lastName' name='lastName'></input>
  </form>
  )
}

export default Form;

Step two: add an onChange handler

At this point the form is still not controlled. The state is waiting to store form data, but there's nothing setup to capture the form data and update state. To do this, we'll need to make sure there's an onChange listener and handler added to the input fields.

import {useState} from 'react';

function Form() {
//Form body
const [formData, setFormData] = useState({
firstName: '',
lastName: ''
});

//changeHandler - takes in an event (e)
//creates a key:value pair from the event properties
//updates the formData state with the pair
function changeHandler(e) {
  const key = e.target.name;
  const val = e.target.value;
  setFormData({...formData,
  [key]: val});
}

//notice the added onChange attributes added to the input tags below
//these will invoke the changeHandler callback
return (
  <form>
    <label for='firstName'>First name</label>
    <input onChange={changeHandler} type='text' id='firstName'></input>
    <label for='lastName'>Last name</label>
    <input onChange={changeHandler} type='text' id='lastName'></input>
  </form>
  )
}

export default Form;

Step three: control that form!

Right now all of the input fields are currently still displaying whatever is typed into them - which, yeah, admittedly does sound a lot like what a data entry field should do. But one of the things that separates a controlled form from a boring ol' ~uncontrollable~ form is that we'd like to control what the form displays. Specifically, we'd like the fields to only display the value that's currently logged into state. We do this by adding a 'value' attribute to the input tags.

import {useState} from 'react';

function Form() {
  //Form body
  const [formData, setFormData] = useState({
  firstName: '',
  lastName: ''
  });

  function changeHandler(e) {
    const key = e.target.name;
    const val = e.target.value;
    setFormData({...formData,
    [key]: val});
  }

  //adding value={formData.property} to each <input>
  return (
    <form>
      <label for='firstName'>First name</label>
      <input value={formData.firstName} onChange={changeHandler} type='text' id='firstName'></input>
      <label for='lastName'>Last name</label>
      <input value={formData.lastName} onChange={changeHandler} type='text' id='lastName'></input>
    </form>
    )
}

export default Form;

Wrapping up

Technically, at this point you have a controlled form! Congratulations! But you probably also want your parent component to be able to access the form data, right? Just like we created a listener-handler pair (from step two: onChange={} and handleChange()) to listen for changes to the input fields, we want to create a listener-handler pair for the form submission event. One key difference is that we're actually going to pass the submitHandler function down to the Form component as a prop. I like to think of this as sending someone down into the well to retrieve the form data - it's stored down there, but it's not coming out unless something else goes down to get it.

Adding the submitHandler to the parent component (App):

import React from 'react';
import Form from './form';

function App() {
  //App body

  function submitHandler(e, formData) {
    //preventDefault will stop the page from refreshing when the form is submitted
    e.preventDefault();
    //do something with the formData
    console.log(formData);
  } 

  return (
    <div id='app container'>
      <Form submitHandler={submitHandler}/>
    </div>
    )
}

export default App;

Now that the submitHandler function is being passed down to the Form component, we want to hook it up to the form element:

import {useState} from 'react';

function Form({submitHandler}) {
  //Form body
  const [formData, setFormData] = useState({
  firstName: '',
  lastName: ''
  });

  function changeHandler(e) {
    const key = e.target.name;
    const val = e.target.value;
    setFormData({...formData,
    [key]: val});
  }

  //add the onSubmit listener
  return (
    <form onSubmit={(e) => submitHandler(e, formData)}>
      <label for='firstName'>First name</label>
      <input value={formData.firstName} onChange={changeHandler} type='text' id='firstName'></input>
      <label for='lastName'>Last name</label>
      <input value={formData.lastName} onChange={changeHandler} type='text' id='lastName'></input>
    </form>
    )
}

export default Form;

Forms: controlled [✓]

And there you have it! Now you've created your very own controlled form, learned a little about how listener-handler pairings empower you to control your form by listening for (and handling!) change events and submit events, and seen how to "send a function down the well" (pass a function as a React prop) to retrieve your formData from a child component. I hope that this helps take some of the mystery out of the process and gives you a general framework to keep in mind as you move forward with your React studies - best of luck!