React useState Hook

The useState Hook is one of the most important Hooks in React.

It allows a function component to remember information between renders and update the user interface when that information changes.

You have already seen the basic idea of state:

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

But useState can do much more than store a simple number.

You can use it to manage:

  • Text input
  • Numbers
  • Boolean values
  • Objects
  • Arrays
  • Form data
  • Selected items
  • UI visibility
  • Loading states
  • Error states
  • Shopping carts
  • User preferences
  • Application filters

For example, a form might use several pieces of state:

const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [isSubmitted, setIsSubmitted] = useState(false);

Or related information can be stored in an object:

const [user, setUser] = useState({
  name: "",
  email: ""
});

Understanding how to choose and update state correctly is essential for building reliable React applications.

Introduction to useState

useState is a React Hook that lets a function component have state.

You import it from React:

import { useState } from "react";

Then call it inside your component:

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

The basic structure is:

useState(initialValue)
       ↓
current value + setter function

For example:

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

creates:

name
↓
Current state

setName
↓
Function that requests a state update

When you call:

setName("Karim");

React will use "Karim" as the state value for a subsequent render.

Why Is useState Called a Hook?

React Hooks are functions that let function components use React features.

useState is one of the built-in Hooks.

Other Hooks include:

useEffect
useContext
useReducer
useRef
useMemo
useCallback

You will learn these in later chapters.

Importing useState

You can import it like this:

import { useState } from "react";

Then:

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

  return <p>{count}</p>;
}

You can also access it through the React namespace:

import * as React from "react";

function App() {
  const [count, setCount] = React.useState(0);

  return <p>{count}</p>;
}

The first style is commonly used because it is concise.

Basic useState

Let’s start with a simple counter.

import { useState } from "react";

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

  return (
    <div>
      <h2>{count}</h2>

      <button onClick={() => setCount(count + 1)}>
        Increase
      </button>
    </div>
  );
}

export default Counter;

The important line is:

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

It means:

  • count is the current state value.
  • setCount updates the state.
  • 0 is the initial state.

Initially:

count = 0

After clicking the button:

count = 1

After another click:

count = 2

React re-renders the component with the new value.

Basic useState Syntax

The general syntax is:

const [state, setState] = useState(initialValue);

For example:

const [name, setName] = useState("John");
const [age, setAge] = useState(25);
const [isOpen, setIsOpen] = useState(false);
const [items, setItems] = useState([]);
const [user, setUser] = useState(null);

The names can be different, but a value / setValue naming pattern makes the code easy to understand.

useState with Strings

Strings are commonly used for:

  • Names
  • Search terms
  • Email addresses
  • Messages
  • Password fields
  • Titles

Example:

import { useState } from "react";

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

  return (
    <div>
      <input
        value={name}
        onChange={(event) => setName(event.target.value)}
        placeholder="Enter your name"
      />

      <p>Hello, {name}</p>
    </div>
  );
}

export default NameInput;

Every time the user types, setName() updates the state.

useState with Numbers

Numbers are useful for counters, quantities, prices, scores, and other numeric values.

import { useState } from "react";

function Score() {
  const [score, setScore] = useState(0);

  return (
    <div>
      <h2>Score: {score}</h2>

      <button onClick={() => setScore(score + 10)}>
        Add 10
      </button>
    </div>
  );
}

export default Score;

useState with Booleans

Boolean state is useful when something has two possible states.

For example:

const [isOpen, setIsOpen] = useState(false);

You can use it for:

  • Open / closed
  • Logged in / logged out
  • Dark mode / light mode
  • Selected / unselected
  • Visible / hidden
  • Enabled / disabled

Example:

import { useState } from "react";

function Menu() {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <div>
      <button
        onClick={() => setIsOpen((current) => !current)}
      >
        {isOpen ? "Close Menu" : "Open Menu"}
      </button>

      {isOpen && (
        <nav>
          <a href="/">Home</a>
          <a href="/about">About</a>
        </nav>
      )}
    </div>
  );
}

export default Menu;

Multiple State Variables

A component can have multiple pieces of state.

For example:

import { useState } from "react";

function Profile() {
  const [name, setName] = useState("");
  const [age, setAge] = useState(0);
  const [isOnline, setIsOnline] = useState(false);

  return (
    <div>
      <input
        value={name}
        onChange={(event) => setName(event.target.value)}
        placeholder="Name"
      />

      <input
        type="number"
        value={age}
        onChange={(event) =>
          setAge(Number(event.target.value))
        }
      />

      <button
        onClick={() => setIsOnline((current) => !current)}
      >
        {isOnline ? "Online" : "Offline"}
      </button>

      <p>Name: {name}</p>
      <p>Age: {age}</p>
    </div>
  );
}

export default Profile;

Here the component has three separate pieces of state:

name
age
isOnline

Each has its own setter.

When Should You Use Multiple State Variables?

Separate state variables can be useful when values:

  • Represent different concepts.
  • Change independently.
  • Have different update logic.
  • Do not naturally belong together.

For example:

const [searchTerm, setSearchTerm] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);

These represent different concepts, so keeping them separate is often clear.

State Variables Must Be Called in the Same Order

Hooks such as useState should be called at the top level of the component, not conditionally.

Good:

function App() {
  const [name, setName] = useState("");
  const [age, setAge] = useState(0);

  // ...
}

Avoid:

function App() {
  if (someCondition) {
    const [name, setName] = useState("");
  }

  // ...
}

Also avoid calling Hooks inside loops or nested functions.

The usual rule is:

Call Hooks at the top level of your React function component or custom Hook.

This allows React to associate each Hook call with the correct state across renders.

Functional State Updates

Sometimes the next state depends on the previous state.

In those situations, use a functional state update.

Instead of:

setCount(count + 1);

you can write:

setCount((currentCount) => currentCount + 1);

The function receives the previous state value.

Basic Example

import { useState } from "react";

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

  function increase() {
    setCount((currentCount) => currentCount + 1);
  }

  return (
    <button onClick={increase}>
      {count}
    </button>
  );
}

export default Counter;

Why Use Functional Updates?

Consider multiple updates:

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

All three calculations use the count value from the current render.

If you want each update to build on the previous update, use:

function increaseThreeTimes() {
  setCount((currentCount) => currentCount + 1);
  setCount((currentCount) => currentCount + 1);
  setCount((currentCount) => currentCount + 1);
}

Now each update receives the appropriate previous state value.

Functional Updates for Booleans

For a toggle:

setIsOpen((currentIsOpen) => !currentIsOpen);

This is often preferable to:

setIsOpen(!isOpen);

when the new value specifically depends on the previous value.

Functional Updates for Arrays

setItems((currentItems) => [
  ...currentItems,
  newItem
]);

Functional Updates for Objects

setUser((currentUser) => ({
  ...currentUser,
  name: "Sarah"
}));

The general pattern is:

setState((previousState) => newState);

State with Objects

You can store an object in state.

For example:

import { useState } from "react";

function Profile() {
  const [user, setUser] = useState({
    name: "John",
    age: 25,
    city: "New York"
  });

  return (
    <div>
      <h2>{user.name}</h2>
      <p>Age: {user.age}</p>
      <p>City: {user.city}</p>
    </div>
  );
}

export default Profile;

You can access individual properties:

user.name
user.age
user.city

Updating One Property

When updating an object in state, create a new object and preserve the properties that should remain unchanged.

setUser((currentUser) => ({
  ...currentUser,
  age: 26
}));

The spread operator:

...currentUser

copies the existing properties.

Then:

age: 26

overwrites the old age.

The resulting object becomes:

{
  name: "John",
  age: 26,
  city: "New York"
}

Why the Spread Operator Is Important

This is not recommended:

user.age = 26;

because it directly mutates the existing state object.

Instead:

setUser((currentUser) => ({
  ...currentUser,
  age: 26
}));

creates a new object.

Updating Multiple Properties

setUser((currentUser) => ({
  ...currentUser,
  name: "Sarah",
  city: "Chicago"
}));

Only the specified properties change.

State with Objects in Forms

Objects can be especially useful for forms.

import { useState } from "react";

function SignupForm() {
  const [form, setForm] = useState({
    name: "",
    email: "",
    password: ""
  });

  function handleChange(event) {
    const { name, value } = event.target;

    setForm((currentForm) => ({
      ...currentForm,
      [name]: value
    }));
  }

  return (
    <form>
      <input
        name="name"
        value={form.name}
        onChange={handleChange}
        placeholder="Name"
      />

      <input
        name="email"
        value={form.email}
        onChange={handleChange}
        placeholder="Email"
      />

      <input
        name="password"
        type="password"
        value={form.password}
        onChange={handleChange}
        placeholder="Password"
      />
    </form>
  );
}

export default SignupForm;

The important part is:

[name]: value

This dynamically updates the property corresponding to the input’s name.

For example, if:

name = "email"

the update becomes conceptually:

{
  ...currentForm,
  email: value
}

This pattern is useful for forms with many fields.

State with Arrays

Arrays are useful when a component needs to remember a collection of items.

Examples include:

  • Shopping cart items
  • Todo items
  • Products
  • Notifications
  • Selected tags
  • Messages
  • Search results

Example:

import { useState } from "react";

function TodoList() {
  const [todos, setTodos] = useState([
    "Learn React",
    "Build a project"
  ]);

  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo}>
          {todo}
        </li>
      ))}
    </ul>
  );
}

export default TodoList;

The array is stored in state:

const [todos, setTodos] = useState([...]);

Adding an Item

Use the spread operator to create a new array:

setTodos((currentTodos) => [
  ...currentTodos,
  "Learn Hooks"
]);

The old array remains unchanged.

Removing an Item

Use filter():

setTodos((currentTodos) =>
  currentTodos.filter(
    (todo) => todo !== "Learn React"
  )
);

Updating an Item

Use map():

setTodos((currentTodos) =>
  currentTodos.map((todo) =>
    todo === "Learn React"
      ? "Learn React Hooks"
      : todo
  )
);

These patterns are fundamental when working with arrays in React state.

Updating Nested Objects

State objects can contain other objects.

For example:

const [user, setUser] = useState({
  name: "John",
  address: {
    city: "New York",
    country: "USA"
  }
});

Suppose you want to update the city.

You should preserve both levels of the object:

setUser((currentUser) => ({
  ...currentUser,
  address: {
    ...currentUser.address,
    city: "Boston"
  }
}));

The outer spread preserves the user properties.

The inner spread preserves the existing address properties.

Then the city is changed.

Why Are Two Spreads Needed?

Consider:

setUser((currentUser) => ({
  ...currentUser,
  address: {
    city: "Boston"
  }
}));

This replaces the entire address object.

If address also contained:

country
postalCode
street

those properties would be lost from the new address object.

Instead:

setUser((currentUser) => ({
  ...currentUser,
  address: {
    ...currentUser.address,
    city: "Boston"
  }
}));

preserves the existing nested properties.

Deeply Nested State

If an object becomes deeply nested, updating it can become difficult:

const [data, setData] = useState({
  user: {
    profile: {
      address: {
        city: "New York"
      }
    }
  }
});

An update may require several levels of copying.

This is one reason to avoid unnecessarily complicated state structures.

If the data does not need to be stored together, separate state or a different state structure may be easier to maintain.

Updating Arrays

There are several common operations when updating arrays in state.

Adding to an Array

Use the spread operator:

setItems((currentItems) => [
  ...currentItems,
  newItem
]);

Example:

setItems((currentItems) => [
  ...currentItems,
  {
    id: 4,
    name: "Monitor"
  }
]);

Adding to the Beginning

setItems((currentItems) => [
  newItem,
  ...currentItems
]);

Removing an Item

Use filter():

setItems((currentItems) =>
  currentItems.filter(
    (item) => item.id !== itemId
  )
);

Updating an Item

Use map():

setItems((currentItems) =>
  currentItems.map((item) =>
    item.id === itemId
      ? {
          ...item,
          completed: true
        }
      : item
  )
);

Replacing an Array

Sometimes you simply need to replace the entire array:

setItems(newItems);

This is appropriate when newItems represents the complete new state.

A Complete Array State Example

import { useState } from "react";

function TodoApp() {
  const [todos, setTodos] = useState([
    {
      id: 1,
      text: "Learn React",
      completed: false
    }
  ]);

  function addTodo() {
    const newTodo = {
      id: Date.now(),
      text: "Build a project",
      completed: false
    };

    setTodos((currentTodos) => [
      ...currentTodos,
      newTodo
    ]);
  }

  function toggleTodo(id) {
    setTodos((currentTodos) =>
      currentTodos.map((todo) =>
        todo.id === id
          ? {
              ...todo,
              completed: !todo.completed
            }
          : todo
      )
    );
  }

  function deleteTodo(id) {
    setTodos((currentTodos) =>
      currentTodos.filter(
        (todo) => todo.id !== id
      )
    );
  }

  return (
    <div>
      <button onClick={addTodo}>
        Add Todo
      </button>

      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>
            <span>
              {todo.text}
            </span>

            <button
              onClick={() => toggleTodo(todo.id)}
            >
              Toggle
            </button>

            <button
              onClick={() => deleteTodo(todo.id)}
            >
              Delete
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default TodoApp;

This demonstrates the three most common array operations:

Add
↓
Spread

Update
↓
map()

Remove
↓
filter()

State Initialization

The value passed to useState() is called the initial state.

For example:

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

The initial state is:

0

Another example:

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

The initial state is an empty string.

You can also initialize objects and arrays:

const [user, setUser] = useState({
  name: "",
  email: ""
});
const [items, setItems] = useState([]);

Initial State Is Used During Initialization

The initial state is used when the component first initializes that state.

For example:

const [count, setCount] = useState(10);

The initial state is 10.

After:

setCount(20);

React does not reset the state back to 10 every time the component renders.

The state remains managed by React.

This distinction is important when you are learning how rendering works.

Initializing State from Props

Sometimes the initial state depends on a prop.

For example:

function Profile({ initialName }) {
  const [name, setName] = useState(initialName);

  return (
    <input
      value={name}
      onChange={(event) => setName(event.target.value)}
    />
  );
}

If the component initially receives:

<Profile initialName="John" />

the initial state will be "John".

However, changing initialName later does not automatically overwrite name.

This is because the prop is being used as the initial value, not as a continuous source of synchronization.

If the component needs to respond to later prop changes, that requires a different design, which may involve derived values or an Effect depending on the actual requirement.

Lazy State Initialization

Sometimes calculating the initial state is expensive.

For example:

const [items, setItems] = useState(createLargeList());

The expression:

createLargeList()

is evaluated when the component function runs.

If the calculation is expensive, you can pass a function instead:

const [items, setItems] = useState(createLargeList);

Notice the difference:

useState(createLargeList())

versus:

useState(createLargeList)

The first calls the function immediately.

The second gives React the initializer function.

Example

function createInitialTodos() {
  console.log("Creating initial todos");

  return [
    {
      id: 1,
      text: "Learn React"
    },
    {
      id: 2,
      text: "Build an app"
    }
  ];
}

function TodoApp() {
  const [todos, setTodos] = useState(createInitialTodos);

  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id}>
          {todo.text}
        </li>
      ))}
    </ul>
  );
}

Here:

useState(createInitialTodos)

tells React to use createInitialTodos to calculate the initial state.

The function should take no arguments and return the initial state.

Why Use Lazy Initialization?

Lazy initialization can be useful when creating the initial state involves noticeable work.

Examples might include:

  • Parsing a large string
  • Reading and processing stored data
  • Performing an expensive calculation
  • Creating a large initial data structure

For a simple value, there is usually no reason to use lazy initialization.

For example:

useState(0)

is perfectly appropriate.

There is no meaningful benefit to:

useState(() => 0)

in such a simple case.

Lazy Initialization with localStorage

A common example is reading a saved preference.

import { useState } from "react";

function ThemeSelector() {
  const [theme, setTheme] = useState(() => {
    return localStorage.getItem("theme") || "light";
  });

  return (
    <button
      onClick={() => {
        const nextTheme =
          theme === "light" ? "dark" : "light";

        setTheme(nextTheme);
        localStorage.setItem("theme", nextTheme);
      }}
    >
      Theme: {theme}
    </button>
  );
}

export default ThemeSelector;

The initializer function reads the saved value when the state is initialized.

This can be useful when the initial value requires work that you do not want to perform unnecessarily on every render.

Lazy Initialization vs Functional State Update

These two concepts can look similar because both involve passing a function to useState or a setter, but they serve different purposes.

Lazy Initialization

const [data, setData] = useState(createData);

The function is used to calculate the initial state.

Functional State Update

setCount((currentCount) => currentCount + 1);

The function is used to calculate the next state from the previous state.

Think of them this way:

useState(function)
       ↓
Calculate initial state


setState(function)
       ↓
Calculate next state

Understanding this difference prevents many beginner mistakes.

useState and Event Handlers

State is often updated inside event handlers.

For example:

import { useState } from "react";

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

  function handleIncrease() {
    setCount((currentCount) => currentCount + 1);
  }

  return (
    <button onClick={handleIncrease}>
      Count: {count}
    </button>
  );
}

export default Counter;

The flow is:

User clicks
     ↓
handleIncrease()
     ↓
setCount()
     ↓
New state
     ↓
React re-renders
     ↓
Updated count

This pattern appears throughout React applications.

useState and Conditional Rendering

State can determine what appears on the screen.

import { useState } from "react";

function App() {
  const [isLoggedIn, setIsLoggedIn] = useState(false);

  return (
    <div>
      {isLoggedIn ? (
        <h1>Welcome back!</h1>
      ) : (
        <h1>Please log in.</h1>
      )}

      <button
        onClick={() =>
          setIsLoggedIn((current) => !current)
        }
      >
        Toggle Login
      </button>
    </div>
  );
}

export default App;

Here, state controls the rendered UI.

useState and Lists

State can hold a list and then map() can render it.

import { useState } from "react";

function Courses() {
  const [courses, setCourses] = useState([
    {
      id: 1,
      title: "React Fundamentals"
    },
    {
      id: 2,
      title: "React Hooks"
    }
  ]);

  return (
    <div>
      {courses.map((course) => (
        <article key={course.id}>
          <h2>{course.title}</h2>
        </article>
      ))}
    </div>
  );
}

export default Courses;

When setCourses() is called with a new array, React renders the list using the updated state.

State Initialization and Strict Mode

When developing with React Strict Mode, React may call certain functions more than once in development to help identify accidental side effects.

This can be noticeable with initializer functions.

For example:

const [items, setItems] = useState(() => {
  console.log("Initializing state");

  return createItems();
});

You may see the initialization-related log more than once in development under Strict Mode.

This does not mean your state is initialized twice in the final production behavior.

The important lesson is that initializer functions should be pure.

Avoid putting side effects such as network requests or subscriptions inside a useState initializer.

Keeping State Updates Pure

A state update should calculate the new state rather than performing unrelated side effects.

For example:

setCount((currentCount) => currentCount + 1);

is a good state update.

Avoid using state updater functions for unrelated side effects.

If you need to synchronize something external with state, such as a subscription or browser API, React’s other mechanisms may be more appropriate.

You will learn about Effects in a later chapter.

Image Placement: How useState Works

Place this image immediately after the “Introduction to useState” section.

Create a modern 16:9 educational infographic showing:

useState()
   ↓
State Value + Setter
   ↓
User Interaction
   ↓
setState()
   ↓
New State
   ↓
React Re-render
   ↓
Updated UI

Include small examples of a counter, input field, and toggle.

Use a modern Web4Learners visual style with clean typography, rounded cards, subtle gradients, modern React/developer visuals, and minimal text.

Image Placement: Objects and Arrays in State

Place this image immediately before the “State with Objects” section.

Create a modern 16:9 educational infographic comparing state containing an object and state containing an array.

Show:

Object State
{
  name,
  email,
  age
}

and:

Array State
[
  item,
  item,
  item
]

Then show:

Object → spread → update property

Array → map/filter/spread → new array

Use a clean, contemporary developer aesthetic with rounded cards and subtle gradients.

Image Placement: Functional State Updates

Place this image immediately after the “Functional State Updates” section.

Create a modern 16:9 visual explaining:

Current State
     ↓
Functional Update
     ↓
Previous State
     ↓
New State

Use a counter example:

0 → +1 → 1
1 → +1 → 2
2 → +1 → 3

Show why functional updates are useful when multiple updates depend on the previous state.

Keep the image modern, minimal, and easy for beginners to understand.

Image Placement: Lazy State Initialization

Place this image immediately after the “Lazy State Initialization” section.

Create a modern 16:9 comparison:

useState(createData())
        ↓
Function runs immediately


useState(createData)
        ↓
React uses function to initialize state

Label the second approach Lazy Initialization.

Use a modern Web4Learners developer aesthetic with clean typography, subtle gradients, rounded cards, and minimal text.

Common useState Mistakes

Forgetting to Import useState

If you write:

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

you need to import it when using the named import style:

import { useState } from "react";

Calling Hooks Inside Conditions

Avoid:

if (isLoggedIn) {
  const [name, setName] = useState("");
}

Call the Hook at the top level instead:

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

Then use the condition when rendering or updating the UI.

Mutating Objects

Avoid:

user.name = "Sarah";

Use:

setUser((currentUser) => ({
  ...currentUser,
  name: "Sarah"
}));

Mutating Arrays

Avoid:

items.push(newItem);

Use:

setItems((currentItems) => [
  ...currentItems,
  newItem
]);

Forgetting the Previous State

If the new state depends on the old state, prefer a functional update:

setCount((currentCount) => currentCount + 1);

Confusing Lazy Initialization with a State Update

These are different:

useState(createData);

and:

setData(createData);

The first tells React to use createData as the initializer.

The second passes createData as the next-state argument. When using a function with a setter, React treats it as an updater function and calls it with the previous state.

If you actually want to store a function as state, you need to wrap it:

const [fn, setFn] = useState(() => myFunction);

This is an advanced detail, but it is useful to understand why functions passed to useState and its setter can have special meaning.

Best Practices for useState

Use Meaningful Names

Good:

const [isLoading, setIsLoading] = useState(false);

Less helpful:

const [x, setX] = useState(false);

Use Functional Updates When Appropriate

If the next state depends on the previous state:

setCount((currentCount) => currentCount + 1);

Avoid Unnecessary State

If something can be calculated from existing state or props, you often do not need another state variable.

For example:

const fullName = `${firstName} ${lastName}`;

may not need its own state.

Don’t Mutate State

Always create new arrays and objects when updating structured state.

Keep State Manageable

If a component has a huge and complicated state structure, consider whether the state can be simplified or whether another state-management approach would make more sense.

Use Lazy Initialization Only When Useful

Do not use lazy initialization just because it exists.

For simple values:

useState(0);

is perfectly fine.

Use an initializer function when calculating the initial state involves meaningful work.

Keep Initializers Pure

An initializer should calculate and return the initial value.

Avoid putting side effects such as API requests inside it.

Real-World Web4Learners Example

Imagine Web4Learners has a tutorial dashboard.

The dashboard needs to remember:

const [searchTerm, setSearchTerm] = useState("");

const [selectedCategory, setSelectedCategory] =
  useState("React");

const [isLoading, setIsLoading] =
  useState(false);

const [tutorials, setTutorials] =
  useState([]);

const [error, setError] =
  useState(null);

Each piece of state represents a different part of the interface.

The user types:

React Hooks

The search state changes.

The user selects:

JavaScript

The selected category changes.

When a request starts:

setIsLoading(true);

When the data arrives:

setTutorials(newTutorials);
setIsLoading(false);

If something goes wrong:

setError("Unable to load tutorials");
setIsLoading(false);

The UI can then respond to these state values.

A simplified component could look like this:

import { useState } from "react";

function TutorialDashboard() {
  const [searchTerm, setSearchTerm] = useState("");
  const [category, setCategory] = useState("React");
  const [tutorials, setTutorials] = useState([]);

  function handleSearch(event) {
    setSearchTerm(event.target.value);
  }

  function handleCategoryChange(event) {
    setCategory(event.target.value);
  }

  return (
    <div>
      <h1>Web4Learners Tutorials</h1>

      <input
        value={searchTerm}
        onChange={handleSearch}
        placeholder="Search tutorials"
      />

      <select
        value={category}
        onChange={handleCategoryChange}
      >
        <option value="React">React</option>
        <option value="JavaScript">JavaScript</option>
        <option value="Python">Python</option>
      </select>

      {tutorials.map((tutorial) => (
        <article key={tutorial.id}>
          <h2>{tutorial.title}</h2>
        </article>
      ))}
    </div>
  );
}

export default TutorialDashboard;

This demonstrates how multiple useState calls can work together to control a real application interface.

Practice Exercise

Build a React Task Manager using useState.

Your application should contain:

  • A task input
  • An Add Task button
  • A task list
  • A completed/uncompleted status
  • A Delete button
  • A task counter

Start with:

const [tasks, setTasks] = useState([]);

Each task should have:

id
text
completed

For example:

{
  id: 1,
  text: "Learn useState",
  completed: false
}

Requirements

When the user adds a task:

Input
 ↓
Add
 ↓
New task added

When the user marks a task as complete:

completed: false
        ↓
completed: true

When the user deletes a task:

Task
 ↓
Removed from array

Use:

map()

to update a task.

Use:

filter()

to remove a task.

Use:

...currentTasks

to add a new task.

Use functional state updates whenever the new state depends on the previous state.

Bonus Challenge

Add:

  • Search
  • Task filtering
  • Total task count
  • Completed task count
  • An empty-state message

For example:

Total Tasks: 5
Completed: 2
Remaining: 3

useState Cheat Sheet

Import

import { useState } from "react";

Basic State

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

String State

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

Boolean State

const [isOpen, setIsOpen] = useState(false);

Array State

const [items, setItems] = useState([]);

Object State

const [user, setUser] = useState({
  name: "",
  email: ""
});

Direct State Update

setCount(10);

Functional Update

setCount((currentCount) => currentCount + 1);

Toggle Boolean

setIsOpen((current) => !current);

Add to Array

setItems((currentItems) => [
  ...currentItems,
  newItem
]);

Remove from Array

setItems((currentItems) =>
  currentItems.filter(
    (item) => item.id !== id
  )
);

Update Array Item

setItems((currentItems) =>
  currentItems.map((item) =>
    item.id === id
      ? { ...item, completed: true }
      : item
  )
);

Update Object

setUser((currentUser) => ({
  ...currentUser,
  name: "Sarah"
}));

Update Nested Object

setUser((currentUser) => ({
  ...currentUser,
  address: {
    ...currentUser.address,
    city: "Boston"
  }
}));

Lazy Initialization

const [data, setData] = useState(createData);

Key Takeaways

The useState Hook is one of the fundamental building blocks of React.

Remember these important points:

  • useState allows function components to remember changing information.
  • Import useState from React when using the named import syntax.
  • useState() returns the current value and a setter function.
  • The value passed to useState() is the initial state.
  • A component can have multiple state variables.
  • Keep separate state variables when values represent independent concepts.
  • Use functional state updates when the next value depends on the previous value.
  • Objects can be stored in state.
  • Use the spread operator when updating part of an object.
  • Nested objects require copying each level that you need to preserve.
  • Arrays can be stored in state.
  • Use spread syntax to add items without mutating the existing array.
  • Use filter() to remove items.
  • Use map() to update individual items.
  • Avoid directly mutating objects or arrays in state.
  • Initial state is used when the state is initialized; it does not continuously synchronize with a prop.
  • Lazy initialization allows React to obtain the initial state from an initializer function.
  • Do not use lazy initialization for simple values unless there is a specific reason.
  • State initializer functions should be pure.
  • Hooks should be called at the top level of components or custom Hooks, not inside conditions or loops.
  • State works closely with events, forms, lists, and conditional rendering.

The most useful patterns to remember are:

Simple value
useState(initialValue)
Previous state
      ↓
Functional update
      ↓
New state
Object
      ↓
Spread existing object
      ↓
Change property
      ↓
New object
Array
      ↓
map / filter / spread
      ↓
New array

And for expensive initial calculations:

useState(createInitialValue)
          ↓
Lazy initialization

Once you are comfortable with useState, you can build interactive forms, counters, menus, task managers, shopping carts, dashboards, search interfaces, and many other React applications.

Leave a Comment