React Basics: useState

February 14, 2024 - Edited on May 27, 2024

useState is arguably the most used React Hook.

It allows you to add interactivity to your components & make your website more engaging.

In this post, you’ll learn everything you need to know about the useState hook, with interactive examples and best practices.

Goal

useState allows you to define a variable inside your component.

The state can then be used to render the UI.

Usage

Using useState is pretty straightforward.

To use it in your application follow these three steps:

1. Import

To get started, import useState :

import { useState } from "react";

2. Definition

Then define a state inside your component:

function MyComponent() {
  const [name, setName] = useState("");
  // ...
}

Here we define a state called name with an initial value of "" (empty string).

Here's the basic syntax:

In a nutshell, useState takes the initial value as an argument and returns a variable and an update function to set the value.

3. Binding

The last step is actually using the state. In other words, bind your state to the UI.

return (
  <input
    type={"text"}
    value={name}
    onChange={(e) => setName(e.target.value)}
  />
)

Putting it all together:


You entered: Youssef

import { useState } from "react";

export function Component() {
  const [name, setName] = useState("Youssef");

  return (
    <div>
      <label>Enter your name</label>
      <input
        type={"text"}
        value={name}
        onChange={(e) => setName(e.target.value)}
      />
      <br />
      <p>
        You entered:{" " + name}
      </p>
    </div>
  );
}

Examples

The use cases for useState are infinite.

Let's start with an easy example.

On/Off Switch

Off

import { useState } from "react";
import { Switch, Text } from "@mantine/core";

function Toggle() {
  const [toggled, setToggled] = useState(false);

  return (
    <Text as="label" size="2">
      <Switch checked={toggled} onClick={() => setToggled(!toggled)} /> 
      {toggled ? "On": "Off"}
    </Text>
  );
}

After defining the state toggled with an initial value of true, we show the user a switch.

Each time the switch is clicked, toggle's value is flipped (truefalsetrue).

The state is used to conditionally render the correct text "On" or "Off".

Moving to a more complex example, a Todo App!

Todo App

Enter a task and hit

import { useState } from "react";

export type TodoItem = {
  text: string;
  checked: boolean;
  id: string;
};

type TodoProps = {
  todo: TodoItem;
  onCheckedChange: () => void;
};
export const Todo = ({
  todo: { checked, text, id },
  onCheckedChange,
}: TodoProps) => {
  return (
    <div className={"flex"}>
      <input
        type="checkbox"
        id={id}
        name="scales"
        checked={checked}
        onChange={onCheckedChange}
      />
      <label htmlFor={id}>{text}</label>
    </div>
  );
};

export function TodoList() {
  const [todos, setTodos] = useState<TodoItem[]>([]);
  const [newTodoText, setNewTodoText] = useState("");

  function onCheckedChange(id: string) {
    setTodos(
      todos.map((todo) =>
        id === todo.id
          ? {
              ...todo,
              checked: !todo.checked,
            }
          : todo,
      ),
    );
  }

  function addNewTodo() {
    const newTodo = {
      text: newTodoText,
      checked: false,
      id: crypto.randomUUID(),
    };
    setTodos([...todos, newTodo]);
    setNewTodoText("");
  }

  return (
    <div>
      {todos.map((todo) => (
        <Todo
          key={todo.id}
          todo={todo}
          onCheckedChange={() => onCheckedChange(todo.id)}
        />
      ))}
      <input
        type={"text"}
        placeholder={"Add Todo..."}
        value={newTodoText}
        onKeyDown={(e) => e.key === "Enter" && addNewTodo()}
        onChange={(e) => setNewTodoText(e.target.value)}
      />
      <p>
        Enter a task and hit <kbd></kbd>
      </p>
    </div>
  );
}

Although a bit more complex, we only use 2 states:

  • todos: An array of todo items. Each Todo has an id, a name, and a checked field.
  • newTodoText: Here we store the user input until submitted.

Pitfalls

While useState seems simple on the surface, there are a lot of pitfalls you should be aware of.

Setting The State Manually

state = false; // ❌ don't
setState(false); // ✅ do

Do not set state manually, always call setState.

The reason, is that setState doesn't only updates the value.

It also notifies React that component needs to be re-rendered.

Working With Arrays and Objects

Same applies for Arrays, don't mutate the array directly:

// ❌ don't
state.add(newElement);
// ✅ do
setState([...state, newElement]);

[...state, newElement] create a new Array with all the old state elements, plus the new Element.

This was React knows that there is a new value to show it to the user.

The same logic applies to objects:

// ❌ don't
state.name = newName;
// ✅ do
setState({ ...state, name: newName });

{...state, name: newName} create a new Object with all the fields from the old state, and overwrites the name.

Calling setState Subsequently

Mini Quiz: Can you guess what sill be shown when you click on the button?

Current count:0

import { useState } from "react";

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

  function onButtonClick() {
    setCount(count + 1);
    setCount(count + 1);
    setCount(count + 1);
  }

  return (
    <div>
      <p>Current count:{count}</p>
      <button onClick={onButtonClick}>Add 3 </button>
    </div>
  );
}

Weird, right?

That's because when you call setState, React doesn't change the state value directly.

Instead, it re-renders your component with the new state.

A quick fix would be to call setState with a function that transforms the old value:

function onButtonClick() {
  setCount((oldCount) => oldCount + 1);
  setCount((oldCount) => oldCount + 1);
  setCount((oldCount) => oldCount + 1);
}

Tips

Using undefined

When initializing a state with undefined, you can just leave out the initial value.

const [state, setState] = useState(undefined);
const [state, setState] = useState(); // same, but shorter

Single Source Of Truth

When working with different states, avoid duplicating data.

// ❌ don't
const [items, setItems] = useState([]);
const [selectedItem, setSelectedItem] = useState();
// ✅ do
const [items, setItems] = useState([]);
const [selectedItemId, setSelectedItemId] = useState();
const selectedItem = items.find((item) => item.id === selectedItemId);

The first approach was duplicating the selected element, you can easily deal with stale data when the items are updated.

The solution is to have the bare minimum state by avoiding duplication.

Typescript Usage

TypeScript is a great way to keep your code type-safe.

useState works seamlessly: You can pass a type parameter to the function:

const [name, setName] = useState<string>("");

If you try to call setState with anything but a string, your editor will scream at you!

But when providing an initial value, TypeScript can infer the type from it.

const [name, setName] = useState("");

So most of the time you can leave the explicit types out, while still having all the goodies from TypeScript!

useState Alternatives

useState is a powerful hook, but don't over use it!

useReducer and useLocalstorage can also be great alternatives.