React PUBLISHED UPDATED

Mastering useEffect - Complete Guide

Table of Contents

Introduction to useEffect

useEffect is a built-in Hook provided by React, primarily used for connecting to external systems such as servers, browser APIs, or third-party libraries (since these aren’t handled by React itself, they’re called external systems).

React function components need to be pure functions, but when we need to perform operations with side effects—like API calls or using third-party libraries—we need to put this code inside useEffect. This solves the negative impacts that side effects can cause:

An important concept when using useEffect is that React function components have “render isolation.” This works by creating a complete “snapshot” for each render, including props, state, event handlers, effects, and all other variables and functions at that moment. Therefore, we must recognize that useEffect, like function components, follows the principle: “each render is independent and has its own everything.” Conceptually, we should think of useEffect as a synchronization tool rather than a lifecycle method.

When useEffect Executes

  1. When a component is mounted, useEffect runs for the first time.

  2. On each component re-render, if any dependency values have changed, the old props and state execute the cleanup function (if any), then the setup function runs with the new props and state.

    More specifically, if useEffect contains a cleanup function, its execution timing will be after the new function component renders the UI, the browser paints, and the user sees the new UI, then it starts cleaning up the previous effect.

    The actual execution order would look like this example:

    1. React renders UI (corresponding to {id: 20})

    2. Browser paints, user sees the new UI

    3. React cleans up the previous effect (still {id: 10})

    4. React runs the new effect (corresponding to {id: 20})

  3. The cleanup function code executes one final time when the component lifecycle ends (unmount).

useEffect accepts two parameters: setup function and dependencies, and can be summarized into three main processing steps

  1. Define an effect function (setup function)

    The setup function contains code for connecting to external systems. Wrapping it in useEffect isolates the timing of side effect execution from the component render process, moving side effect handling to after each render flow completes, avoiding side effects directly blocking UI generation and updates.

  2. Add a cleanup function to the setup function to clean up side effects (if needed)

    If cleanup logic is needed, you can return a cleanup function from the setup function. This allows developers to define side effects within component functions while also specifying how to clean up the impacts of those side effects through the “cleanup function.” The cleanup function runs before each side effect re-execution and when the component unmounts, preventing the impacts of side effects from accumulating. The specific approach is to return another function as the cleanup function within the effect function and handle side effect cleanup or reversal within it.

  3. Specify dependencies

    The dependencies parameter is an optional array that can include props, state, or any variables used in the component. When provided, React uses the Object.is algorithm for comparison—if any value in dependencies differs from the previous time, this useEffect will re-execute. This allows useEffect to specify a dependency array for the effect function to skip unnecessary side effect processing.

    However, it’s important to note that dependencies are a performance optimization, not execution timing control. They’re used to determine “when it’s safe to skip” rather than specifying “only when it will execute.” When dependencies haven’t updated, the behavior of “skipping side effect execution” isn’t absolutely guaranteed, so please don’t lie about dependencies!!! Following this principle, React officially provides a linter tool specifically to help developers detect and even automatically fix hooks dependencies. This ESLint React hooks linter rule is already built into integrated development environments like Create React App or Next.js. Additionally, we need to install the corresponding ESLint plugin in our code editor to see linter warning prompts when dependencies have issues and use the corresponding auto-fix functionality.

    Also, “not providing dependencies parameter” and “providing an empty array [ ] as dependencies parameter” have completely different meanings and execution effects.

    • Not providing dependencies parameter: Maintains useEffect’s default behavior, meaning the effect function executes once after every render.

    • Providing an empty array [ ] as dependencies parameter: Indicates this effect function doesn’t depend on any data, so the component can safely skip effect function execution on every re-render.

useEffect Usage Strategies

”Honestly include all component values used inside effects in the dependency array.” This is the first method to consider when dealing with dependency issues.

Specific example:

When you want to avoid unnecessary effect re-execution, “using functional updates” is the preferred option.

Usage conditions

Specific example

When encountering two state variables with interdependent values that prevent using functional updates, “using useReducer” is the solution.

Specific example:

When you need to calculate the next state based on props, use useReducer and “define the reducer inside the component” as the solution.

Specific example:

**If certain functions are only used inside effects, “**move the functions into the effect**”

Specific example:

If functions are used by multiple effects and don’t use any component scope data (props, state, etc.), “lift the functions outside the component

Specific example:

If functions are used by multiple effects and need to access component internal state or props, “use the useCallback Hook

Specific example:

This solution also applies to function props passed down from parent components:

useCallback essentially “adds another layer of dependency checking.” It doesn’t avoid function dependencies but makes the function itself change only when necessary.

useCallback allows functions to become part of the data flow by linking the function’s identity to its dependencies:

If useEffect contains asynchronous methods that support cancellation, remember to cancel async requests in the cleanup function.

Specific example:

If useEffect contains asynchronous methods that don’t support cancellation, the simplest workaround is using a boolean variable to track component validity.

Specific example: