Paramclasses Academy - Blog

Tutorials Details

←
Post 13 of 29
→

What is useEffect Hook and Component Lifecycle Concepts in React.js ?

What is useEffect Hook and Component Lifecycle Concepts in React.js ?

October 29, 2026

1. Introduction

In React, components often need to synchronize with external systems such as APIs, timers, browser event listeners, subscriptions, and third-party libraries.

The useEffect Hook allows functional components to perform this synchronization after React commits updates to the screen.

For example, useEffect can help you:

  • Fetch data from an API.

  • Update the browser document title.

  • Start and clean up a timer.

  • Add and remove event listeners.

  • Subscribe to an external data source.

  • Synchronize a component with an external system when its inputs change.

Important: Not every operation belongs inside useEffect. Calculating a value from props or state can usually be done directly during rendering. Actions caused by a specific user interaction, such as submitting a form, usually belong in the corresponding event handler.

2. Learning Objectives

After completing this tutorial, you will be able to:

  1. Understand the purpose of the useEffect Hook.

  2. Understand the syntax and dependency array.

  3. Explain how effects run when dependencies change.

  4. Use cleanup functions correctly.

  5. Understand React component lifecycle concepts.

  6. Work with timers and browser event listeners.

  7. Handle asynchronous effects safely.

  8. Avoid common useEffect mistakes.

3. Core Concepts and Explanation

3.1 What Is the useEffect Hook?

useEffect is a React Hook used to synchronize a component with an external system.

Basic syntax:

import { useEffect } from "react";

useEffect(() => {
  // Synchronize with an external system

  return () => {
    // Clean up when necessary
  };
}, [dependencies]);

The dependency array controls when React re-synchronizes the effect.

The cleanup function is optional. When provided, React runs it before the effect is set up again with changed dependencies and when the component is removed from the screen.

3.2 The Three Dependency Patterns

Pattern 1: No dependency array

useEffect(() => {
  console.log("Effect executed after a commit");
});

The effect runs after every commit of the component, including its initial commit.

Pattern 2: Empty dependency array

useEffect(() => {
  console.log("Effect setup");
}, []);

The effect runs after the initial commit in ordinary production behavior. If cleanup is returned, it runs when the component unmounts.

In development, React Strict Mode may run an additional setup-and-cleanup cycle to help detect unsafe effects.

Pattern 3: Dependencies provided

useEffect(() => {
  console.log("Username changed:", username);
}, [username]);

The effect runs after the initial commit and again when username changes according to React's dependency comparison.

Dependency pattern When the effect runs
No array After every commit
[] After the initial commit
[value] After the initial commit and when value changes

3.3 Understanding Component Lifecycle Concepts

React components commonly go through three broad stages:

  • Mount: The component is added to the screen.

  • Update: The component renders again because props, state, or another relevant input changes.

  • Unmount: The component is removed from the screen.

Effects help manage synchronization across these stages. However, useEffect is not simply a direct replacement for every class lifecycle method. Its primary purpose is to start and stop synchronization with external systems.

3.4 Understanding the Cleanup Function

Consider an effect that starts a timer:

useEffect(() => {
  const timerId = setInterval(() => {
    console.log("Timer running");
  }, 1000);

  return () => {
    clearInterval(timerId);
  };
}, []);

The cleanup function prevents the interval from continuing after the component is removed.

Cleanup is also useful for:

  • Removing event listeners.

  • Unsubscribing from data sources.

  • Disconnecting external connections.

  • Cancelling or ignoring outdated asynchronous work.

3.5 How Dependencies Work

Every reactive value used by an effect should be included in its dependency array. Reactive values include props, state, and variables or functions declared inside the component.

Example:

useEffect(() => {
  console.log(`Current count: ${count}`);
}, [count]);

When count changes, React re-runs the effect.

Do not omit dependencies merely to force an effect to run less frequently. Missing dependencies can cause stale values and bugs. Instead, restructure the code so the effect only depends on what it actually needs.

4. Practical Code Examples

Example 1: Update the Browser Document Title

Create or update src/App.jsx:

import { useEffect, useState } from "react";

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

  useEffect(() => {
    document.title = `Count: ${count}`;
  }, [count]);

  return (
    <div>
      <h1>useEffect Example</h1>
      <h2>Count: {count}</h2>

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

export default App;

Explanation:

  1. useState stores the counter value.

  2. useEffect synchronizes the browser tab title with that value.

  3. The effect runs again when count changes.

  4. The document title updates without manually manipulating the DOM to render the React UI.

Example 2: Timer with Cleanup

import { useEffect, useState } from "react";

function Timer() {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    const timerId = setInterval(() => {
      setSeconds((previousSeconds) => previousSeconds + 1);
    }, 1000);

    return () => {
      clearInterval(timerId);
    };
  }, []);

  return (
    <div>
      <h2>Timer: {seconds} seconds</h2>
    </div>
  );
}

export default Timer;

Explanation:

  • setInterval() schedules a callback every second.

  • The functional state update uses the latest state value.

  • clearInterval() stops the timer during cleanup.

  • When the component unmounts, its timer does not continue running.

Example 3: Responding to a Window Resize

import { useEffect, useState } from "react";

function WindowWidth() {
  const [width, setWidth] = useState(window.innerWidth);

  useEffect(() => {
    function handleResize() {
      setWidth(window.innerWidth);
    }

    window.addEventListener("resize", handleResize);

    handleResize();

    return () => {
      window.removeEventListener("resize", handleResize);
    };
  }, []);

  return (
    <div>
      <h2>Browser Width: {width}px</h2>
    </div>
  );
}

export default WindowWidth;

Explanation:

  1. The component reads the current browser width.

  2. It registers a resize event listener.

  3. When the browser is resized, the state is updated.

  4. Cleanup removes the same listener function.

This example assumes the component runs in a browser environment.

Example 4: Fetching Data Safely

Effects are sometimes used to fetch data. The following example ignores outdated results if the component's userId changes before a request finishes.

import { useEffect, useState } from "react";

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState("");

  useEffect(() => {
    let ignore = false;

    async function loadUser() {
      setLoading(true);
      setError("");
      setUser(null);

      try {
        const response = await fetch(
          `https://jsonplaceholder.typicode.com/users/${userId}`
        );

        if (!response.ok) {
          throw new Error("Failed to fetch user");
        }

        const data = await response.json();

        if (!ignore) {
          setUser(data);
        }
      } catch (err) {
        if (!ignore) {
          setError(err.message);
        }
      } finally {
        if (!ignore) {
          setLoading(false);
        }
      }
    }

    loadUser();

    return () => {
      ignore = true;
    };
  }, [userId]);

  if (loading) return <p>Loading user...</p>;
  if (error) return <p>Error: {error}</p>;
  if (!user) return <p>No user found.</p>;

  return (
    <div>
      <h2>{user.name}</h2>
      <p>Email: {user.email}</p>
    </div>
  );
}

export default UserProfile;

Explanation:

  • The effect runs when userId changes.

  • The asynchronous function is declared inside the effect.

  • The ignore flag prevents an outdated request from updating the component's state.

  • Loading, error, and success states are handled separately.

The ignore flag does not cancel the network request itself. For actual request cancellation, use an AbortController with fetch() and abort it during cleanup. In larger applications, a data-fetching library or framework-supported data-loading approach can provide caching and additional request management.

Example 5: A Reusable Effect with a Changing Value

import { useEffect, useState } from "react";

function SearchPreview() {
  const [search, setSearch] = useState("");

  useEffect(() => {
    document.title = search
      ? `Searching: ${search}`
      : "Search Preview";
  }, [search]);

  return (
    <div>
      <h2>Search Preview</h2>

      <input
        type="text"
        value={search}
        onChange={(event) => setSearch(event.target.value)}
        placeholder="Enter a search term"
      />

      <p>You are searching for: {search || "Nothing yet"}</p>
    </div>
  );
}

export default SearchPreview;

The effect synchronizes the document title with the current search input. The live preview itself is derived directly from state during rendering, so it does not require another effect.

5. Real-World Applications

The useEffect Hook is useful in many React applications.

E-commerce website

  • Synchronize the page title with the selected product.

  • Subscribe to an external inventory update service.

  • Connect to third-party widgets.

Admin dashboard

  • Listen for browser events.

  • Subscribe to real-time notifications.

  • Start and clean up polling when appropriate.

Weather application

  • Fetch weather data when the selected city changes.

  • Ignore or cancel outdated requests.

  • Manage loading and error states.

Chat application

  • Connect to a messaging service.

  • Subscribe to incoming messages.

  • Unsubscribe when the conversation changes or the component unmounts.

Important: An effect should be used when synchronizing with an external system, not simply because some code needs to run after a state update.

6. Common Mistakes and Best Practices

Mistake 1: Forgetting the Dependency Array

useEffect(() => {
  console.log("Effect executed");
});

This runs after every commit. If the effect updates state on every run, it may cause repeated renders or an infinite loop.

Best practice: Choose dependencies based on the values the effect uses.

Mistake 2: Omitting a Dependency

useEffect(() => {
  console.log(userId);
}, []);

If userId changes, the effect may continue using the old value.

Best practice: Include userId in the dependency array when the effect depends on it.

Mistake 3: Forgetting Cleanup

useEffect(() => {
  window.addEventListener("resize", handleResize);
}, []);

This does not remove the event listener.

Best practice: Return a cleanup function that removes the listener.

Mistake 4: Using an Effect for Derived Values

Avoid this unnecessary pattern:

const [firstName, setFirstName] = useState("Amit");
const [lastName, setLastName] = useState("Sharma");
const [fullName, setFullName] = useState("");

useEffect(() => {
  setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);

Prefer deriving the value directly:

const [firstName, setFirstName] = useState("Amit");
const [lastName, setLastName] = useState("Sharma");

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

This avoids an unnecessary state update and additional render.

Mistake 5: Making the Effect Callback async

Avoid:

useEffect(async () => {
  const response = await fetch("/api/data");
}, []);

An async function returns a Promise, but React expects an effect callback to return either a cleanup function or nothing.

Prefer:

useEffect(() => {
  async function loadData() {
    const response = await fetch("/api/data");
    const data = await response.json();

    console.log(data);
  }

  loadData();
}, []);

For production code, also handle errors and stale or cancelled requests.

Mistake 6: Misunderstanding Strict Mode

In development, React Strict Mode may run an additional effect setup-and-cleanup cycle to expose bugs. This behavior is intended to help you verify that cleanup correctly reverses the setup.

Best practice: Make effects safe to set up and clean up rather than using flags to suppress the extra development cycle.

Best-Practice Checklist

  • Include all required reactive dependencies.

  • Return cleanup functions for subscriptions, timers, and event listeners.

  • Avoid effects for values that can be calculated during rendering.

  • Handle asynchronous errors and outdated requests.

  • Do not assume effects run before the browser paints.

  • Keep each effect focused on a clear synchronization responsibility.

  • Use React's Hooks lint rules to detect dependency problems.

7. Practice Exercises

Exercise 1: Document Title

Create a counter that updates the browser tab title whenever the count changes.

Exercise 2: Timer

Build a timer that increases every second and clears its interval when the component unmounts.

Exercise 3: Window Width

Display the current browser width and update it when the window is resized. Make sure to remove the event listener.

Exercise 4: User Profile

Fetch a user from an API when userId changes. Show loading, error, and success states, and prevent outdated requests from replacing newer results.

Exercise 5: Find the Mistake

Identify the problem in this code:

useEffect(() => {
  setCount(count + 1);
}, []);

Think about why the effect only uses the initial count value and why adding count as a dependency would create repeated updates.

8. Key Takeaways and Conclusion

In this tutorial, you learned:

  • useEffect synchronizes React components with external systems.

  • Dependency arrays determine when effects re-synchronize.

  • Cleanup functions stop or reverse synchronization.

  • Mounting, updating, and unmounting describe broad component lifecycle stages.

  • Asynchronous effects need proper error handling and protection against outdated results.

  • Derived values and event-specific actions often do not need useEffect.

  • React Strict Mode helps expose effect setup and cleanup problems during development.

Final takeaway: Understand what an effect synchronizes with, identify its dependencies, and ensure its cleanup reverses any setup that must not remain active.

←
Post 13 of 29
→