DEV Community

Max
Max

Posted on • Edited on

React Controlled Forms

Intro

Nearly all web applications use forms. Everything from logging into social media, making new posts, and customizing your profile uses forms. React provides a useful tool that allows us to create controlled forms: forms whose component state (what the internal code tracks) matches the user interface state (what the user sees in the application).

Quick Review

In React, state is data that is dynamic. It changes over time as users interact with the application. And it is used in controlled forms to give the values for each input.

To create controlled forms you should first be familiar with React Hooks, specifically the State Hook. React Hooks are special predefined functions within the React library. The State Hook, 'useState', allows developers to access/'hook into' React's internal state of our components.

useState

To use the State Hook, we first need to import it from the React library.

import React, { useState } from "react";

function Component () {
    const [stateVar, setStateVar] = useState(""):

    return <h1>{stateVar}</h1>;
}
Enter fullscreen mode Exit fullscreen mode

Once imported, we can use the State Hook by invoking the useState function. This function returns an array of two elements: a state variable and a setter function. When invoking useState, you can declare the state variable's initial value, in this case an empty string.

To update a state variable, you must invoke the setter function. After a setter function is invoked, the state is set asynchronously and upon completion, the component is rerendered. If we were to call setStateVar("Hello World") our program will begin to asynchronously set the state variable to "Hello World" and upon rerender our h1 will read Hello world.

Although there is more to the State Hook, we can move onto how they are used to create controlled forms.

Controlled Forms

Controlled forms are forms that derive the values for its inputs from state. Let's look at a simple example of a controlled form:

import React, { useState } from "react";

function Form() {
  const [firstName, setFirstName] = useState("John");
  const [lastName, setLastName] = useState("Henry");

  return (
    <form>
      <input type="text" value={firstName} />
      <input type="text" value={lastName} />
      <button type="submit">Submit</button>
    </form>
  );
}

Enter fullscreen mode Exit fullscreen mode

The above example is a controlled form because the values for the input fields are defined by their respective state variables. But our program as it is would not allow a user to change the values of these inputs. So how do we allow for users to actually fill out the form and change the state of our application?

Input fields can listen for many different event types; for this instance we will use onChange. onChange will trigger every time the value of the input changes.

<input type="text" onChange={handleFirstNameChange} value={firstName} />
Enter fullscreen mode Exit fullscreen mode

Now everytime the user alters the input element, it will call the function handleFirstNameChange and pass the event object as an argument of the function.

function handleFirstNameChange(event) {
  setFirstName(event.target.value);
}
Enter fullscreen mode Exit fullscreen mode

From this event argument, we can access the value currently entered in our input element: event.target.value and use our setter function to update our state variable. Adding a similar function for our lastName input and state variable would allow the user to interact with the form while changing state within the component.

Abstractions

In our previous example, we only had two state variables. But in a world where forms often ask for passwords, addresses, emails, phone numbers, etc., creating unique state variables and invoking unique callback functions for each, while doable, can easily become repetitve. It may be preferable to have a single state object variable.

For simplicity we will build off our previous example with only the firstName and lastName state variables. But we will consolidate them into one object variable.

import React, { useState } from "react";

function Form() {
  //const [firstName, setFirstName] = useState("John");
  //const [lastName, setLastName] = useState("Henry");
    const [userObj, setUserObj] = useState({firstName: "John" , 
                                            lastName: "Henry"})

  return (
    <form>
      <input type="text" value={userObj.firstName} />
      <input type="text" value={userObj.lastName} />
      <button type="submit">Submit</button>
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

Here you can see we replaced our two state variables with one by creating the nameObj state variable with the default value of an object with keys firstName and lastName.

Making these input fields dynamic requires a bit more complexity than using individual callback handleChange functions, but it is very useful and easy once you get used to it.

<input type="text" id = "firstName" onChange={handleUserChange} value={userObj.firstName} />
<input type="text" id = "lastName" onChange={handleUserChange} value={userObj.lastName} />
Enter fullscreen mode Exit fullscreen mode

Like in our previous build, we have an onChange event listener that takes a callback handler. But this time we have added an id prop which will be used in our event handler to determine which field is being updated.

Unlike our previous build, we cannot simply call the event handler and directly set the state variable to the value of the current state. Since the same event handler is being used on two different inputs, we need a way to differentiate each input to know which value we intend to update. Additionally, we need to be careful not to simply use setUserObj(event.target.value) as this would mean the whole state variable would become a single value, no longer having both a first and last name.

function handleUserChange(event) {
    const key = event.target.id
    const value = event.target.value
  //setFirstName(event.target.value);
    setUserObj({ ...userObj , [key]: value })
}

Enter fullscreen mode Exit fullscreen mode

In the event handler, we can grab the target element's id and set it as our key since they match the keys of our userObj. Likewise we can save our target element's value to a variable as well. Finally, we invoke our setter function setUserObj with argument {...userObj , [key]: value }. We use the spread operator to copy our current state of the userObj and use our key and value pair to update our desired object property. Doing so will update only our intended object while leaving all others in their current state.

Creating one state variable object can help organize your code and make it easier to add and track additional fields to your form.

Why Use Controlled forms?

Now you know how to create controlled forms, but why use them? One common use is for data validation.

Validation

Users often make mistakes while filling out forms. But controlled forms allow us to catch these mistakes and alert the user so as to allow the application to be used as intended. In our previous example, we had inputs where we intended to capture a user's first and last name. But suppose a user inputted a number for their first name. Our code as it was would allow that input and we could end up with a user who's name appears as "34Mmy 5m1th". As l33t as that is, we may not want to allow users to do this.

Controlled forms can validate the data a user enters before actually setting state.

import React, { useState } from "react";

function Form() {
    const [userObj, setUserObj] = useState({firstName: "John" , 
                                            lastName: "Henry"})
function handleUserChange(event) {
    if (event.target.value === "" || isNaN(event.target.value)){
    const key = event.target.id
    const value = event.target.value
    setUserObj({ ...userObj , [key]: value })
} 
    else{
    console.log("Input is not valid")
}
}
  return (
    <form>
   <input type="text" id = "firstName" onChange={handleUserChange} value={userObj.firstName} />
<input type="text" id = "lastName" onChange={handleUserChange} value={userObj.lastName} />
      <button type="submit">Submit</button>
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

Within our handleUserChange function, we added an if else statement to validate our target's input. In this case, we allow our userObj to update if the input's value is an empty string or not a number. Otherwise, we print out an error to our console. You could write an alternative else statement to allow for some notification to the user, but for this simple example we will leave it with a simple console.log for the dev.

Top comments (1)

Collapse
 
brense profile image
Rense Bakker

Few tips, although in your example it would not have any big performance impact, you should probably change the handleUserChange function to use the React useCallback hook. This will also highlight another problem, namely that you need to provide the userObj state variable to the callback dependencies, which means the function would change everytime the userObj changes (currently your function changes every render).

const handleUserChange = useCallback((event) => {
  // your code requires the userObj state variable in the dependencies array
  // which means this function is going to change everytime the userObj changes
}, [userObj])
Enter fullscreen mode Exit fullscreen mode

However, if you're using state functions like this, you dont have to use the state variable itself, you can use the state parameter inside the set state function like this:

const handleUserChange = useCallback((event) => {
  // other logic...
  const { id: key, value } = event.target
  setUserObj(currentUserObj => ({ ...currentUserObj, [key]: value }))
}, [])
Enter fullscreen mode Exit fullscreen mode