DEV Community

Jim Grimes
Jim Grimes

Posted on • Edited on

The useState Hook and Forms in Controlled Components

Hello again! The past several weeks, my classmates and I have been learning how create websites using React. For this blog, I will be discussing an example of how to make forms in controlled components in React.

Creating Forms in Controlled Components

For the purpose of this discussion, I have made a simple app to list and view and search through the pizzas available at my favorite pizzeria:

function App() {
    const [pizzas, setPizzas] = useState([]);

    useEffect(() => {
        fetch("http://localhost:3001/pizzas")
        .then(resp => resp.json())
        .then(setPizzas)
    }, [])

    return (
        <div>
            <SearchBar />
            <AddPizza />
            <PizzaList pizzas={pizzas} />
        </div>
    );
}

Enter fullscreen mode Exit fullscreen mode

This parent component, App, has three child components:

  1. SearchBar, which contains a text input form used to search through the available pizzas,
  2. AddPizza, which contains a form that can be used to add pizzas to the list of available pizzas, and
  3. PizzaList, which displays the names and descriptions of the pizzas available at the pizzeria.

We are getting information about the available pizzas from a JSON database. The pizza information will need to be used by each of the child components of App, either to list the pizzas, add a new pizza, or search through the pizzas. Therefore, the useState hook is used to store the pizza information in App, the closest common ancestor of the three child components. The pizza information stored in state is passed as a prop to PizzaList, where the names and descriptions of pizzas are displayed.

The basic elements of AddPizza and SearchBar look like this:

function AddPizza({handleAddPizza}) {

    return (
        <form onSubmit={handleSubmit}>
            <input 
                type="text" 
                name="name" 
                placeholder="Enter Pizza Name" 
            />
            <input 
                type="text" 
                name="description" 
                placeholder="Enter Pizza Description" 
            />
            <input type="submit" value="Add Pizza" />
        </form>
    )
}
Enter fullscreen mode Exit fullscreen mode
function SearchBar({handlePizzaSearch}) {

    return (
        <div>
            <input 
                type="text" 
                placeholder="Search Pizzas" 
            />
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

Both of these components contain text input fields. We need to plan out: (1) where we are going to store the text a user types into the forms and (2) where the event actions are going to be handled. In other words, we have to decide how to use state and props in a way that works best with our website.

In React, Controlled Components are those in which form’s data is handled by the component’s state. It takes its current value through props and makes changes through callbacks like onClick, onChange, etc. A parent component manages its own state and passes the new values as props to the controlled component.

https://www.geeksforgeeks.org/controlled-vs-uncontrolled-components-in-reactjs/

As the React documentation notes, we don't always have to make a component completely controlled or completely uncontrolled. We can customize how state and props are used based on our needs.

In practice, “controlled” and “uncontrolled” aren’t strict technical terms—each component usually has some mix of both local state and props. However, this is a useful way to talk about how components are designed and what capabilities they offer.

https://react.dev/learn/sharing-state-between-components#controlled-and-uncontrolled-components

In this example, one of the forms will have state stored in the parent component and also receive a callback function as a prop, while the other form will use local state but still receive a callback function as a prop.

The Search Bar: Passing Props to Child Component with State in Parent Component

When a user types in the search bar, the list of pizzas will be filtered to find pizza names that match the user's input. The search does not wait for the user to hit a 'submit' button or any other action to trigger the search. It filters the list of pizzas as the user types.

We will store the user's input in state so that the state can be used to in our filter function. Because the text input in SearchBar must be used in real time to filter the list of pizzas, we should establish our state in App. Then, since our state is stored in App, we will filter the list of pizzas in App, and pass the filtered list to PizzaList to render the search results.

With that functionality added, the updated sections of App and SearchBar look like this:

function App() {
    const [pizzas, setPizzas] = useState([]);
    const [searchText, setSearchText] = useState("");

    <!-- useEffect omitted to keep example shorter -->

    function handleAddPizza(newPizza) {
        setPizzas([
        ...pizzas,
        newPizza
        ])
    }

    const filteredPizzas = pizzas.filter(pizza => pizza.name.toLowerCase().includes(searchText.toLowerCase()))

    return (
        <div>
        <SearchBar handlePizzaSearch={handlePizzaSearch} />
        <AddPizza handleAddPizza={handleAddPizza} />
        <PizzaList pizzas={filteredPizzas} />
        </div>
    );
}
Enter fullscreen mode Exit fullscreen mode
function SearchBar({handlePizzaSearch}) {

    function handleSearchChange(event) {
        handlePizzaSearch(event.target.value)
    }

    return (
        <div>
            <input
                type="text"
                placeholder="Search Pizzas"
                onChange={handleSearchChange}
            />
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

The user's search input is stored in App as the searchText state. When the page loads, searchText is an empty string. As the user types, it is changed by setSearchText to whatever the user types into the search bar. The .filter() method is used to filter the list of pizzas, selecting only those pizzas with names that include the text typed into the search bar. To avoid requiring an exact match of capitalization, the filter changes both the pizza name and the search input to all lower case. Finally, instead of passing the original pizza state to the PizzaList component, the list of filtered pizzas is passed. If searchText is an empty string, like when the page first loads, every pizza name will be a match so every pizza will be included in the filtered list.

Adding a Pizza: A Mixture of Local State and Props

The next component, AddPizza, has a form with two text inputs and a submit button. The text inputs should be updated to reflect what the user types in, but we do not want to try to update or change the pizza database until the user hits the 'submit' button.

For this component, we can save its state locally, in AddPizza. We will create a function in App that will update the pizzas state when the new pizza is submitted. This function will be passed to AddPizza as a prop. When the user clicks the 'submit' button, the completed form data will be sent to the database in a POST request. In turn, the response from the JSON database will be an object containing the new pizza information plus a unique ID number. We can then pass that object to the callback function from App so that it can be added to pizzas in App. The updated, relevant sections of App and AddPizza are:

function App() {
    const [pizzas, setPizzas] = useState([]);

    <!-- state for searchText and useEffect omitted for brevity -->

    function handleAddPizza(newPizza) {
        setPizzas([
            ...pizzas,
            newPizza
        ])
    }

    <!-- functions for handlePizzaSearch and filteredPizzas omitted -->

    return (
        <div>
            <SearchBar handlePizzaSearch={handlePizzaSearch} />
            <AddPizza handleAddPizza={handleAddPizza} />
            <PizzaList pizzas={filteredPizzas} />
        </div>
    );
}
Enter fullscreen mode Exit fullscreen mode
function AddPizza({handleAddPizza}) {
    const [formData, setFormData] = useState({
        name: "",
        description: ""
    })

    function handleFormChange(event) {
        setFormData({
            ...formData,
            [event.target.name]: event.target.value
        })
    }

    function handleSubmit(event) {
        event.preventDefault();
        fetch("http://localhost:3001/pizzas", {
            method: "POST",
            headers: {
                "Content-Type": "application/json"
            },
            body: JSON.stringify(formData)
        })
        .then(resp => resp.json())
        .then(newPizza => handleAddPizza(newPizza))
    }

    return (
        <form onSubmit={handleSubmit}>
            <input 
                type="text" 
                name="name" 
                placeholder="Enter Pizza Name" 
                value={formData.name} 
                onChange={handleFormChange} 
            />
            <input 
                type="text" 
                name="description" 
                placeholder="Enter Pizza Description" 
                value={formData.description} 
                onChange={handleFormChange} 
            />
            <input type="submit" value="Add Pizza" />
        </form>
    )
}
Enter fullscreen mode Exit fullscreen mode

Both form text input fields begin with default values of empty strings. As the user types in the form, the name and value of the form field in which they type are used to update the section of formData that corresponds to the form field. The values in the formData object are used as the values shown in the form's text input so that the text the user sees matches the information stored in state. When a value in formData is an empty string, such as when the page first loads, the placeholder text is visible in the text input field.

After the new pizza is added to the database, we are using the response from JSON POST request to update the web page's list of pizzas, instead of using the information stored in state as formData. That is so the pizza added to pizzas will include a unique ID number which can be used as the key for the JSX list.

Finally, when we update pizzas in App to include the new pizza, we do so by making a copy of the current array stored in pizzas, not by simply adding the new pizza to the current array. By making a copy, React recognizes that we have changed the state and re-renders the page in order to reflect the new state.

Conclusion

Planning out how and where to use state is an important part of working with React components that include forms. Approaches similar to the ones outlined above can be used with toggle buttons, drop-down menus, and other features that require you to store input values in state and update the information on a page based changes to state. Thanks for reading!

Top comments (0)