How to Use the useState() Hook in React – Explained with Code Examples

The useState hook is one of the most important tools in a React developer‘s toolkit. It allows functional components to have state, which is critical for building dynamic, interactive user interfaces.

According to the State of JS 2022 survey, React is used by over 80% of front-end developers, and hooks are used by 94% of React developers. Understanding how to use useState effectively is a must-have skill.

State of JS 2022 React Usage

In this in-depth guide, we‘ll cover everything you need to know to master the useState hook, including:

  • What state is and why it‘s important
  • How to use the useState hook
  • Updating objects and arrays in state
  • Working with multiple state variables
  • Best practices and common pitfalls
  • Related hooks and resources

Let‘s get started!

What is State and Why Does It Matter?

In React, state refers to data that can change over time and affect what is rendered on the screen. Some common examples of state include:

  • Form input values
  • Current selected tab or accordion item
  • Data fetched from an API
  • Whether a modal is open or closed

Whenever the state changes, React automatically re-renders the component to reflect the new state. This declarative approach to UI development is what makes React so powerful and popular.

Before hooks were introduced in React 16.8, only class components could have state. Functional components were limited to receiving props and returning JSX. The introduction of hooks like useState changed that, allowing developers to add state to functional components.

How to Use the useState Hook

Using the useState hook is straightforward. Here are the key steps:

  1. Import useState from the react package
  2. Call useState inside your functional component, passing the initial state value
  3. Destructure the returned array into a state variable and a function to update it
  4. Use the state variable in your JSX
  5. Call the update function to change the state value as needed

Here‘s a simple counter example putting this all together:

import React, { useState } from ‘react‘;

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

In this example:

  1. We import useState from react
  2. Inside the Counter component, we call useState(0) to initialize a state variable count to 0
  3. We destructure the returned array into count and setCount variables
  4. We display the current count value in a <p> tag
  5. When the button is clicked, we call setCount(count + 1) to increment the count by 1

Every time setCount is called, React will re-render the Counter component, updating the displayed count value.

Updating Objects and Arrays in State

In addition to primitive values like numbers and strings, you can also store objects and arrays in state using useState. However, updating them requires a bit more care.

Consider an example where we have an array of todo items in state:

import React, { useState } from ‘react‘;

function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: ‘Learn React‘, completed: false },
    { id: 2, text: ‘Build a project‘, completed: false },
    { id: 3, text: ‘Write an article‘, completed: false },
  ]);

  function toggleTodo(id) {
    const updatedTodos = todos.map((todo) => {
      if (todo.id === id) {
        return { ...todo, completed: !todo.completed };
      }
      return todo;
    });
    setTodos(updatedTodos);
  }

  return (
    <ul>
      {todos.map((todo) => (
        <li
          key={todo.id}
          style={{ textDecoration: todo.completed ? ‘line-through‘ : ‘none‘ }}
          onClick={() => toggleTodo(todo.id)}
        >
          {todo.text}
        </li>
      ))}
    </ul>
  );
}

In this example, we have an array of todo objects in state. When a todo is clicked, we want to toggle its completed status.

However, we can‘t simply mutate the todos array directly, like this:

function toggleTodo(id) {
  const todoToUpdate = todos.find((todo) => todo.id === id);
  todoToUpdate.completed = !todoToUpdate.completed; // Don‘t do this!
  setTodos(todos); 
}

This mutates the state directly, which can lead to unexpected bugs. Instead, we need to create a new array with the updated todo object.

The toggleTodo function does this by:

  1. Using map to loop through the todos array
  2. If the current todo.id matches the id passed to the function, returning a new object with the completed status flipped
  3. Otherwise, returning the original todo object unchanged
  4. Passing the new updatedTodos array to setTodos

By creating a new array instead of mutating the existing one, we ensure that React correctly detects the state change and re-renders the component.

The same principle applies when updating objects in state. Always create a new object instead of mutating the existing one.

Working with Multiple State Variables

As your components grow in complexity, you may find yourself needing to manage multiple pieces of related state. There are a few different approaches you can take.

Using a Single State Object

One option is to store all related state in a single object, like this:

import React, { useState } from ‘react‘;

function Form() {
  const [form, setForm] = useState({
    name: ‘‘,
    email: ‘‘,
    message: ‘‘,
  });

  function handleChange(event) {
    const { name, value } = event.target;
    setForm((prevState) => ({
      ...prevState,
      [name]: value,
    }));
  }

  function handleSubmit(event) {
    event.preventDefault();
    console.log(form);
    // Submit form data to server...
  }

  return (
    <form onSubmit={handleSubmit}>
      <label>
        Name:
        <input
          type="text"
          name="name"
          value={form.name}
          onChange={handleChange}
        />
      </label>
      <label>
        Email:
        <input
          type="email"
          name="email"
          value={form.email}
          onChange={handleChange}
        />
      </label>
      <label>
        Message:
        <textarea
          name="message"
          value={form.message}
          onChange={handleChange}
        />
      </label>
      <button type="submit">Submit</button>
    </form>
  );
}

In this example, we store the name, email, and message form fields in a single form state object.

The handleChange function uses the name attribute of the input that triggered the change event to dynamically update the corresponding state property using computed property names.

While this approach works, it can become cumbersome as the number of fields grows.

Using Multiple State Variables

An alternative approach is to use a separate state variable for each form field:

import React, { useState } from ‘react‘;

function Form() {
  const [name, setName] = useState(‘‘);
  const [email, setEmail] = useState(‘‘);  
  const [message, setMessage] = useState(‘‘);

  function handleSubmit(event) {
    event.preventDefault();
    console.log({ name, email, message });
    // Submit form data to server...
  }

  return (
    <form onSubmit={handleSubmit}>
      <label>
        Name:
        <input
          type="text"
          value={name}
          onChange={(e) => setName(e.target.value)}
        />
      </label>
      <label>
        Email:
        <input
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
        />
      </label>
      <label>
        Message:
        <textarea
          value={message}
          onChange={(e) => setMessage(e.target.value)}
        />
      </label>
      <button type="submit">Submit</button>
    </form>
  );
}

With this approach, each form field has its own state variable and update function. The onChange handlers are inlined directly in the JSX.

This can make the JSX a bit more verbose, but it‘s often easier to reason about and maintain, especially as the form grows larger.

Ultimately, the approach you choose depends on your specific use case and personal preference. Both are valid ways to manage form state with useState.

Best Practices and Common Pitfalls

Here are a few best practices and gotchas to keep in mind when working with useState:

Deceptively Simple State

It‘s easy to fall into the trap of thinking that all your state should be simple primitives like strings and numbers. In reality, many components need to manage more complex state shapes like objects and arrays.

Don‘t be afraid to use objects and arrays in state when it makes sense for your component. Just remember to update them immutably.

Keep State as Localized as Possible

In general, you want to keep state as close to where it‘s being used as possible. Don‘t hoist state up to a parent component unless it‘s truly necessary.

Lifting state up

Component did update life cycle

Naming conventions for state variables

Resources for going deeper (React docs, Dan Abramov blog post, Dave Ceddia etc)

Similar Posts