Skip to Content
Lesson 4

State Management with useState

Workplace Context

Imagine building an interactive feature, like a dropdown menu that opens and closes, an “Add to Cart” button that updates a counter, or a form where the user types input. These UI elements need to remember information (Is the menu open? How many items are in the cart? What text is in the input field?) and change over time based on user interactions. This internal memory that can change is called state. In React, managing state is essential for creating dynamic and responsive user interfaces. This lesson introduces the fundamental React Hook, useState, which allows your components to manage this internal memory.

Learning Objectives

By the end of this lesson, learners will be able to:

  • Explain the concept of component state in React.
  • Differentiate between props and state and identify when to use each.
  • Import and utilize the useState Hook to add state to functional components.
  • Read the current state value within a component.
  • Update state using the state setter function provided by useState.
  • Understand that updating state triggers a component re-render.
  • Explain the importance of immutability when updating state, especially for objects and arrays.
  • Apply immutable update patterns for objects and arrays in state.
  • Define state types explicitly using TypeScript with useState.

What is State?

While props allow you to pass data down from a parent, state is data that is managed internally by a component. It represents information that can change over the component’s lifecycle, often due to user interaction or other events (like data fetching).

Think of state as a component’s private memory. When this memory changes, React automatically re-renders the component (and potentially its children) to reflect the updated information in the UI.

Examples of state:

  • The current value entered in an input field.
  • Whether a checkbox is checked or unchecked.
  • Whether a dropdown menu is open or closed.
  • The list of items added to a shopping cart.
  • Data fetched from an API.

Props vs. State

It is crucial to understand the difference:

FeaturePropsState
SourcePassed from parent componentManaged internally within the component
MutabilityRead-only (immutable within the child)Can be changed using a setter function
PurposeConfiguration, passing data downManaging dynamic data, user interaction
AnalogyFunction argumentsLocal variables within a function (but persistent across renders)

Rule of Thumb: If a component needs to “remember” some information that can change over time based on interactions within that component, that information likely belongs in state. If a component simply needs to display data provided by its parent, that data should come via props.


Introduction to Hooks and useState

Hooks are special functions introduced in React 16.8 that let you “hook into” React features from functional components. Before Hooks, managing state or lifecycle events required using class components. Hooks allow you to use state and other React features without writing classes.

The most fundamental Hook is useState. It allows you to add a state variable to your functional component.

Syntax:

import { useState } from 'react'; // Import useState from React function MyComponent() { // Call useState inside the component function // It takes one argument: the initial state value // It returns an array with two elements: // 1. The current state value // 2. A function to update the state value const [stateVariable, setStateVariable] = useState(initialValue); // ... component logic ... return ( // ... JSX using stateVariable ... // ... Elements that call setStateVariable (e.g., in an onClick) ... ); }

Explanation:

  1. Import: You must import useState from the 'react' package.
  2. Call useState: Call useState at the top level of your functional component (not inside loops, conditions, or nested functions).
  3. Initial Value: Pass the desired initial value for this piece of state as the argument to useState. This is used only during the first render.
  4. Return Value (Array Destructuring): useState returns an array containing exactly two items:
    • stateVariable (you choose the name): Holds the current value of the state for this render cycle.
    • setStateVariable (conventionally set + VariableName): The function you call to update the state value. Calling this function tells React to re-render the component with the new state value.

Reading and Updating State

Let’s build a simple counter:

Editor
Loading...
Preview

Explanation:

  1. Initialization: const [count, setCount] = useState(0); sets up a state variable count initialized to 0.
  2. Reading: Inside the JSX, {count} reads the current value of the count state variable.
  3. Updating: The increment and decrement functions call setCount(), passing the new desired value (count + 1 or count - 1).
  4. Re-rendering: When setCount() is called, React:
    • Schedules a re-render of the Counter component.
    • During the re-render, the useState(0) line will not use 0 again; instead, it will return the latest updated value for count.
    • The JSX is evaluated again with the new count value, updating the displayed paragraph.
  5. Functional Updates: The “Add 5” button uses a functional update: setCount(prevCount => prevCount + 5). This is the safest way to update state when the new state depends on the previous state, especially if updates might be batched. React guarantees prevCount will be the correct, up-to-date value.

State Updates Can Be Asynchronous/Batched: React may batch multiple state updates together for performance reasons. This means you should not rely on reading the state variable immediately after calling the setter function within the same event handler and expect to see the new value. Always use the functional update form (setState(prevState => ...)) when the new state depends on the old one.


The Importance of Immutability

Immutability means not changing data directly. Instead of modifying an existing object or array, you create a new object or array with the desired changes.

Why is immutability crucial for React state?

  • Change Detection: React primarily uses reference equality (===) to check if state has changed and if a re-render is needed. If you mutate an object or array directly (changing its contents but not the object/array reference itself), React might not detect the change and skip the re-render.
  • Predictability & Debugging: Immutable updates make state changes easier to track and debug (e.g., using time-travel debugging tools).
  • Performance Optimizations: Helps React optimize re-renders.

How to Update State Immutably:

  1. Primitives (Numbers, Strings, Booleans): These are naturally immutable. Just pass the new value to the setter function.

    const [isActive, setIsActive] = useState(false); setIsActive(true); // Correct const [name, setName] = useState("Guest"); setName("Admin"); // Correct
  2. Objects: Create a new object using the spread syntax (...) to copy the old properties, then overwrite the properties you want to change.

    interface UserSettings { theme: 'light' | 'dark'; fontSize: number; } const [settings, setSettings] = useState<UserSettings>({ theme: 'light', fontSize: 16 }); // Incorrect: Mutating state directly! React might not re-render. // settings.theme = 'dark'; // setSettings(settings); // Correct: Create a new object with the change const updateTheme = () => { setSettings(prevSettings => ({ ...prevSettings, // Copy all properties from the previous settings theme: 'dark' // Overwrite the theme property })); };
  3. Arrays: Create a new array. Common methods include:

    • Adding an item: Spread syntax [...prevArray, newItem] or [newItem, ...prevArray]
    • Removing an item: .filter() method prevArray.filter(item => item.id !== idToRemove)
    • Updating an item: .map() method prevArray.map(item => item.id === idToUpdate ? { ...item, property: newValue } : item)
    interface Todo { id: number; text: string; completed: boolean; } const [todos, setTodos] = useState<Todo[]>([ { id: 1, text: 'Learn React', completed: false } ]); // Add a new todo const addTodo = (text: string) => { const newTodo: Todo = { id: Date.now(), text, completed: false }; // Use timestamp for simple unique ID setTodos(prevTodos => [...prevTodos, newTodo]); // Create new array }; // Toggle completion status const toggleTodo = (id: number) => { setTodos(prevTodos => prevTodos.map(todo => todo.id === id ? { ...todo, completed: !todo.completed } : todo // Create new array, update matching todo ) ); }; // Remove a todo const removeTodo = (id: number) => { setTodos(prevTodos => prevTodos.filter(todo => todo.id !== id)); // Create new array };
🚫
Important

Always use the setter function (setStateVariable) provided by useState. Never modify the state variable directly.


Typing State with useState

TypeScript can often infer the type of the state variable from the initialValue.

const [count, setCount] = useState(0); // TypeScript infers 'count' is type 'number' const [name, setName] = useState("Alice"); // TypeScript infers 'name' is type 'string' const [isActive, setIsActive] = useState(false); // TypeScript infers 'isActive' is type 'boolean'

However, sometimes you need to be explicit, especially if the initial state might be null, undefined, or if the state can hold multiple types (union types) or is a complex object/array:

interface User { id: number; name: string; } // State that might initially be null, but will hold a User object later const [user, setUser] = useState<User | null>(null); // State that holds an array of strings const [tags, setTags] = useState<string[]>([]); // State for complex object (interface defined elsewhere) const [config, setConfig] = useState<AppSettings>({ volume: 80, notifications: true });

Use the angle bracket syntax useState<StateType>(initialValue) to provide an explicit type.


Activity 1: Simple Counter

Recreate the counter component shown earlier from scratch, attempting to reference the code as little as possible. Additionally:

  • Experiment with the increment, decrement, reset, and “Add 5” buttons. What other functions might you include?
  • Create a second counter that increments or decrements based on the value of the first counter.
    • For example, if the first counter’s value is 5, the second counter should increment by 5 when the “increment” button is pressed.

Here is a simple example that meets the above requirements:

Counter One

Current Count: 0


Counter Two

Current Count: 0


Activity 2: Toggle Visibility

Create a component ShowHideMessage that has a button “Toggle Message”. When the button is clicked, it either shows or hides a <p> tag containing some text (e.g., “Secret message revealed!”). Use useState to manage the visibility state (a boolean).

Challenge: Modify your code to fade the message in and out using CSS transitions based on a class or style applied conditionally.

Here is a simple example that meets the above requirements:

Toggle Visibility Example

Secret message revealed! 🎉


Knowledge Check

What is component state in React?

  • Select an answer to view feedback.

What does the useState Hook return?

  • Select an answer to view feedback.

Why is immutability important when updating state in React?

  • Select an answer to view feedback.

How should you update an object in state to change one of its properties?

  • Select an answer to view feedback.

Summary

In this lesson, you learned about component state - the internal, changeable memory of a component that drives dynamic UIs. You contrasted state with props and understood their distinct roles. We introduced the fundamental useState Hook, covering its syntax, how to initialize state, read its current value, and update it using the setter function. Crucially, you learned about the importance of immutability and practiced creating new objects and arrays when updating state, rather than modifying them directly. Finally, you saw how to provide explicit types for state using TypeScript. Mastering useState is the first major step towards building interactive React applications.


References

Additional Resources