Skip to Content
Lesson 1

useState and useEffect

Workplace Context

As you build more sophisticated React applications, you’ll find that managing component state and handling side effects become increasingly complex. For instance, you might need to update state based on its previous value in rapid succession, manage forms with interconnected fields, or interact with browser APIs and external data sources. A deeper understanding of useState’s nuances and the capabilities of useEffect for managing side effects like data fetching, subscriptions, or manual DOM manipulations after rendering, are crucial for creating robust, performant, and maintainable React components. This lesson provides that deeper understanding, enabling you to tackle these common yet advanced scenarios effectively.

Learning Objectives

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

  • Explain and utilize functional updates with useState for reliable state transitions.
  • Understand how React batches state updates and its implications.
  • Implement complex state management patterns using useState for objects and arrays, ensuring immutability.
  • Describe the purpose and fundamental syntax of the useEffect hook for managing side effects.
  • Utilize the useEffect dependency array to control when effects are re-executed.
  • Implement cleanup functions within useEffect to prevent memory leaks and unwanted behavior.
  • Identify and implement common useEffect patterns such as running effects once on mount or responding to prop/state changes.

Deep Dive into useState

In React Fundamentals, you were introduced to the useState hook as the primary way to add state to functional components. Let’s explore some of its more advanced aspects.

Recap: useState Basics

Quickly, useState is a Hook that lets you add React state to function components.

import { useState } from 'react'; function Counter() { const [count, setCount] = useState(0); // count is the state, setCount is the updater return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div> ); }

You call it with an initial state, and it returns an array containing the current state value and a function to update that state.

Functional Updates

When your new state depends on the previous state, you can pass a function to the state setter. This function will receive the previous state as its argument and should return the new state. This is known as a functional update.

Why are functional updates important?

  1. Correctness with Asynchronous Updates: State updates in React can be asynchronous and batched for performance. If you update state multiple times in a single event handler based on the current state variable directly, you might be working with a stale value. Functional updates ensure you’re always using the most recent state.
  2. State Batching: React often groups multiple state updates into a single re-render to improve performance. Functional updates are crucial when an update relies on the value from a previous update within the same batch.

Syntax: setCount(prevState => prevState + newValue);

Editor
Loading...
Preview

State Batching

React’s performance strategy includes batching state updates. This means that when you call setState multiple times within a single synchronous event handler (like a button click), React doesn’t re-render the component for each setState call. Instead, it “batches” them together and performs a single re-render at the end of the event.

Since React 18, automatic batching is more extensive and consistent, applying to updates inside of timeouts, promises, native event handlers, or any other event. This generally improves performance out-of-the-box. Functional updates are still best practice when the next state depends on the previous one, ensuring correctness regardless of batching behavior.

Complex State Management with useState

useState isn’t limited to primitive values like numbers or strings. You can also manage objects and arrays. However, it’s crucial to remember the principle of immutability: never modify state directly. Always create a new copy of the object or array with the changes.

Managing Objects

When updating an object in state, use the spread syntax (...) to copy the existing properties and then override the ones you want to change.

Editor
Loading...
Preview

Managing Arrays

Similarly, for arrays, use immutable operations:

  • Adding: setMyArray(prevArray => [...prevArray, newItem])
  • Removing: setMyArray(prevArray => prevArray.filter(item => item.id !== itemIdToRemove))
  • Updating: setMyArray(prevArray => prevArray.map(item => item.id === itemIdToUpdate ? { ...item, property: newValue } : item))
Editor
Loading...
Preview

The useEffect Hook

While useState allows components to remember things, many applications need to perform actions that go beyond managing state directly within the component’s rendering logic. These are called side effects.

What are Side Effects?

Side effects are operations that interact with the “outside world” from within your component. Common examples include:

  • Data fetching from an API
  • Setting up or tearing down subscriptions (e.g., to timers, WebSockets, browser events)
  • Manually changing the DOM (though this should be rare in React)
  • Logging to the console
  • Setting the document title

These actions shouldn’t block rendering and often need to happen after React has updated the DOM.

Introduction to useEffect

The useEffect Hook lets you perform side effects in function components. You pass it a function (the “effect”), and React will run this function after performing the DOM updates.

Basic Syntax: useEffect(() => { /* Your effect code here */ }, [dependencies]);

  • The first argument is a function that contains the side effect logic.
  • The second argument (optional) is the dependency array.

The Dependency Array

The dependency array is crucial for controlling when your effect re-runs.

  1. No Dependency Array (Runs after every render):

    • Example: useEffect(() => { console.log('Component rendered or updated'); });
    • If you omit the dependency array, the effect runs after the initial render and after every subsequent re-render of the component.
    • This is often a source of bugs (like infinite loops if the effect itself triggers a state update that causes a re-render). Use with caution.
  2. Empty Dependency Array [] (Runs once after initial render):

    • Example: useEffect(() => { console.log('Component did mount (initial render)'); }, []);
    • This tells React to run the effect only once, after the initial render. This is common for:
      • Initial data fetching.
      • Setting up subscriptions that should last the lifetime of the component.
  3. Dependency Array with Values [propA, stateB] (Runs when dependencies change):

    • Example: useEffect(() => { console.log(`Value of propA or stateB changed: ${propA}, ${stateB}`); }, [propA, stateB]);
    • The effect will run after the initial render and then again only if any of the values in the dependency array have changed between renders. React usesObject.is comparison to check for changes.
    • This is vital for effects that depend on certain props or state to avoid unnecessary re-execution.
Editor
Loading...
Preview
Console

Open browser consoleTerminal

🚫
Important

Always include all values from your component scope (props, state, functions) that are used inside the useEffect callback in its dependency array. If you omit a dependency, your effect might capture stale values, leading to bugs. ESLint plugins (like eslint-plugin-react-hooks) often help enforce this.

Cleanup Functions

Some side effects need to be “cleaned up” when the component unmounts or before the effect runs again. For example:

  • Clearing timers (clearInterval, clearTimeout)
  • Unsubscribing from event listeners (window.removeEventListener)
  • Canceling API requests or closing WebSocket connections

To perform cleanup, you can return a function from your useEffect callback. This cleanup function will be executed:

  1. Before the component unmounts.
  2. Before the effect runs again (if its dependencies have changed).

Syntax:

useEffect(() => { // Effect setup const timerId = setInterval(() => { console.log('Timer tick'); }, 1000); // Cleanup function return () => { clearInterval(timerId); console.log('Timer cleaned up'); }; }, []); // Empty array: runs once on mount, cleans up on unmount
Editor
Loading...
Preview

In TimerComponent, if isActive changes, the previous interval is cleared before a new one (or none) is set up. If ShowHideTimer hides TimerComponent, its cleanup function will also run.

Common useEffect Patterns (Introduction)

While Lesson 2 will dive deeper, here are a few common patterns:

  1. Fetching Data on Mount:

    useEffect(() => { fetch('https://api.example.com/data') .then(response => response.json()) .then(data => console.log(data)); }, []); // Empty array: fetch once

    (We’ll improve this with loading/error states later.)

  2. Subscribing to Browser Events:

    useEffect(() => { const handleResize = () => console.log('Window resized'); window.addEventListener('resize', handleResize); return () => { window.removeEventListener('resize', handleResize); }; }, []);
  3. Updating Document Title:

    useEffect(() => { document.title = \`Current Page: \${pageName}\`; }, [pageName]);

Activity 1: Form with State Dependencies

Task: Build a form component (RegistrationForm) with the following fields:

  • username (text input)
  • password (password input)
  • confirmPassword (password input)
  • email (email input, optional)
  • subscribeToNewsletter
    • checkbox, only enabled if email is provided and valid
    • basic validation like includes ’@’ is sufficient

Requirements:

  1. Manage form state using useState. You can choose individual state variables or a single state object.
  2. The confirmPassword field should show an error message (e.g., a simple <p> tag) if its value doesn’t match the password field. This check should happen as the user types.
  3. The subscribeToNewsletter checkbox should only be enabled if the email field contains text and includes an ”@” symbol.
  4. Implement functional updates for state if one field’s validation depends on another.
  5. On submit, console.log the form data (if valid).

Completed Example:


Activity 2: Data Fetching Component with useEffect

Task: Create a component (PostFetcher) that fetches a single post from the JSONPlaceholder API (https://jsonplaceholder.typicode.com/posts/1) when it mounts.

Requirements:

  1. Use useState to manage:
    • post (to store the fetched post data, initially null or undefined).
    • loading (boolean, initially true).
    • error (to store any error message, initially null).
  2. Use useEffect to fetch the data when the component mounts.
    • Set loading to false after the fetch completes (successfully or with an error).
    • If successful, store the fetched post in the post state.
    • If there’s an error, store the error message in the error state.
  3. Render the UI:
    • If loading is true, display “Loading post…”.
    • If error is present, display the error message.
    • If post data is available, display its title and body.
  4. Make it interactive:
    • Add a button to fetch a different post (e.g., by changing an ID in state, which useEffect should then depend on).

Completed Example:

Current Post ID: 1

Loading post...


Knowledge Check

When using useState and updating state based on its previous value, why is it recommended to use a functional update (e.g., setCount(prevCount => prevCount + 1))?

  • Select an answer to view feedback.

What does an empty dependency array ([]) in a useEffect hook signify?

  • Select an answer to view feedback.

When should you provide a cleanup function in useEffect?

  • Select an answer to view feedback.

If you have useEffect(() => { console.log(myProp); }, []), and myProp changes, will the console log the new value?

  • Select an answer to view feedback.

Summary

In this lesson, we took a deeper look at the useState hook, emphasizing functional updates for reliable state transitions and immutable patterns for managing complex state like objects and arrays. We then introduced useEffect, a powerful hook for handling side effects such as data fetching, subscriptions, and manual DOM changes. You learned how the dependency array controls when effects run and the importance of cleanup functions for preventing memory leaks and managing resources. These concepts are foundational for building more dynamic and robust React applications.


References

Additional Resources