Popular Searches
Popular Course Categories
Popular Courses

Understanding Side Effects

Understanding Side Effects

React Side Effects and Lifecycle

Understanding Side Effects in React JS – Complete Notes

Side effects are operations that interact with something outside the normal rendering process of a React component. Examples include fetching data from an API, setting timers, adding event listeners, updating the browser tab title, and connecting to an external service. Understanding side effects helps developers build React applications that are predictable, efficient, and easier to maintain.

React commonly uses the useEffect Hook to synchronize components with external systems. However, not every side effect belongs inside useEffect. Actions caused directly by user interactions, such as submitting a form or purchasing a product, generally belong in event handlers.

Official Documentation: React – Synchronizing with Effects


1. What Are Side Effects?

A side effect is an operation that does more than calculate and return a value. It interacts with or changes something outside the current calculation.

In React, rendering should be pure. A component should calculate its JSX from its current props and state without directly modifying external systems during rendering.

Examples of Side Effects

  • Fetching information from an API.
  • Starting or stopping a timer.
  • Adding or removing browser event listeners.
  • Updating the browser document title.
  • Connecting to a chat server or WebSocket.
  • Interacting with third-party JavaScript libraries.
  • Playing or pausing a video through a DOM API.
  • Writing data to browser storage.

Simple Definition

A side effect is an operation that interacts with something outside a component's pure rendering calculation.


2. Why Are Side Effects Important in React?

React components can render multiple times when props or state change. If external operations are performed directly during rendering, they may happen more often than intended and cause bugs.

Managing side effects in the correct place helps separate UI calculation from external operations.

Benefits of Proper Side Effect Management

  • Predictable rendering: Components calculate UI without unexpected external changes.
  • Better performance: Effects can be configured to run only when their relevant dependencies change.
  • Resource management: Timers, listeners, and connections can be cleaned up.
  • Fewer bugs: Correct cleanup helps prevent duplicate subscriptions and stale asynchronous results.
  • Maintainable code: Rendering, user actions, and external synchronization remain separate.

3. Pure Rendering vs Side Effects

Pure Rendering Side Effect
Calculates JSX from props and state. Interacts with an external system.
Should not change external values. May change external values or establish connections.
Should return the same result for the same inputs, assuming the same rendering conditions. May perform actions such as network requests or event subscriptions.
Example: displaying a user's name. Example: subscribing to a server connection.

Example: Pure Rendering

function Welcome({ name }) {
  return <h1>Welcome, {name}!</h1>;
}

export default Welcome;

This component calculates and returns JSX from the name prop. It does not perform an external operation.

Example: Incorrect Side Effect During Rendering

function Counter() {
  document.title = "Counter Page";

  return <h1>Counter</h1>;
}

Updating document.title directly during rendering is an external mutation. If the title needs to stay synchronized with the component, an Effect is a better choice.

Correct Approach

import { useEffect } from "react";

function Counter() {
  useEffect(() => {
    document.title = "Counter Page";
  }, []);

  return <h1>Counter</h1>;
}

export default Counter;

The Effect synchronizes the browser tab title after React commits the component.


4. Understanding the useEffect Hook

The useEffect Hook allows a React component to synchronize with an external system. It is commonly used for subscriptions, timers, browser APIs, connections, and other external operations.

Syntax

import { useEffect } from "react";

useEffect(() => {
  // Setup or synchronization logic

  return () => {
    // Optional cleanup logic
  };
}, [dependencies]);

Parts of useEffect

Part Description
Effect function Contains the setup or synchronization logic.
Cleanup function Optionally undoes setup, such as removing a listener or disconnecting a service.
Dependencies array Specifies reactive values that determine when the Effect must run again.

Important: useEffect returns undefined. The function passed to it may return a cleanup function.


5. How useEffect Works

React runs an Effect after committing the component's changes to the screen. When dependencies change, React runs the previous cleanup function, if provided, before running the updated Effect.

Basic Flow

Component Renders
       |
       v
React Commits Changes
       |
       v
Effect Setup Runs
       |
       v
Dependency Changes?
       |
       +---- Yes ----> Previous Cleanup Runs
       |                       |
       |                       v
       |                New Effect Runs
       |
       +---- No ----> Effect Is Skipped

Example

import { useState, useEffect } from "react";

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

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

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

export default Counter;

Explanation

  • useState stores the counter value.
  • The Effect synchronizes the browser title with the current count.
  • The [count] dependency tells React to rerun the Effect when the count changes.
  • The button click updates state and causes the component to render again.

6. Understanding the Dependency Array

The dependency array controls when an Effect needs to run again. React compares each dependency with its previous value using Object.is.

Syntax Behavior
useEffect(() => {}) Runs after every commit.
useEffect(() => {}, []) Runs after the initial commit, with an additional development-only setup and cleanup cycle in Strict Mode.
useEffect(() => {}, [value]) Runs after the initial commit and again when value changes.
useEffect(() => {}, [name, count]) Runs after the initial commit and when either dependency changes.

Example with Dependencies

import { useEffect, useState } from "react";

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

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

  return (
    <div>
      <h2>Hello, {name}</h2>
      <button onClick={() => setName("Sam")}>
        Change Name
      </button>
    </div>
  );
}

When the name changes, React runs the Effect again because name is included in the dependency array.

Best practice: Include all reactive values used by the Effect. Do not remove dependencies just to prevent an Effect from running. Instead, restructure the code so the Effect expresses the intended synchronization correctly.


7. Common Types of Side Effects

7.1 Fetching Data from an API

Fetching data communicates with a server, so it is an external operation. An Effect can be appropriate when data synchronization depends on a component's props or state. In larger applications, framework data-loading features or data-fetching libraries may be more suitable.

import { useState, useEffect } from "react";

function UsersList() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState("");

  useEffect(() => {
    const controller = new AbortController();

    async function loadUsers() {
      try {
        setLoading(true);
        setError("");

        const response = await fetch(
          "https://jsonplaceholder.typicode.com/users",
          { signal: controller.signal }
        );

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

        const data = await response.json();
        setUsers(data);
      } catch (err) {
        if (err.name !== "AbortError") {
          setError(err.message || "Something went wrong");
        }
      } finally {
        if (!controller.signal.aborted) {
          setLoading(false);
        }
      }
    }

    loadUsers();

    return () => {
      controller.abort();
    };
  }, []);

  if (loading) return <p>Loading users...</p>;
  if (error) return <p>Error: {error}</p>;

  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

export default UsersList;

Explanation

  • fetch() requests user information from the API.
  • AbortController allows the request to be cancelled when the Effect is cleaned up.
  • The response is checked before its data is used.
  • Loading and error states provide feedback to the user.

Note: An Effect-based fetch is useful for learning, but production applications may benefit from framework loaders or data-fetching libraries that provide caching, request deduplication, and race-condition handling.

7.2 Using Timers

Timers are external browser resources. When a component starts an interval, it should clear that interval during cleanup.

import { useEffect, useState } from "react";

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

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

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

  return <h2>Seconds: {seconds}</h2>;
}

export default Timer;

The interval increases the counter every second. The cleanup function clears the interval when the component is removed.

7.3 Adding Event Listeners

When a component listens for browser events, it should remove its listener when the subscription is no longer needed.

import { useEffect, useState } from "react";

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

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

    window.addEventListener("resize", handleResize);

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

  return <h2>Window Width: {width}px</h2>;
}

export default WindowWidth;

This browser-only example updates the displayed width when the window is resized. Applications rendered on the server should avoid accessing window during rendering and should initialize browser-dependent state safely.

7.4 Updating the Browser Title

import { useEffect } from "react";

function PageTitle({ title }) {
  useEffect(() => {
    document.title = title;
  }, [title]);

  return <h1>{title}</h1>;
}

export default PageTitle;

The browser title is synchronized with the title prop whenever it changes.

7.5 Connecting to an External Service

import { useEffect } from "react";

function ChatRoom({ roomId, createConnection }) {
  useEffect(() => {
    const connection = createConnection(roomId);

    connection.connect();

    return () => {
      connection.disconnect();
    };
  }, [roomId, createConnection]);

  return <h2>Chat Room: {roomId}</h2>;
}

export default ChatRoom;

The component connects to the selected room and disconnects when the room or connection factory changes, or when the component is removed. The connection factory should have a stable identity when appropriate to avoid unnecessary reconnections.


8. Understanding Cleanup Functions

A cleanup function reverses or releases resources established by an Effect. It is important for preventing duplicate subscriptions, unnecessary timers, and outdated asynchronous operations.

When Is Cleanup Needed?

  • When starting a timer or interval.
  • When adding an event listener.
  • When opening a connection or subscription.
  • When starting an animation that needs to be reset.
  • When a request should be aborted or its result ignored.

Example

useEffect(() => {
  const handleOnline = () => {
    console.log("Browser is online");
  };

  window.addEventListener("online", handleOnline);

  return () => {
    window.removeEventListener("online", handleOnline);
  };
}, []);

Cleanup Lifecycle

  1. React runs the Effect setup after the component commits.
  2. When dependencies change, React runs the previous cleanup first.
  3. React then runs the new Effect setup.
  4. When the component is removed, React runs the final cleanup.

Not every Effect needs cleanup. For example, setting a browser title to a value usually does not require a cleanup function. Add cleanup when the setup establishes a resource or behavior that must be undone.


9. Side Effects vs Event Handlers

Not every side effect should be placed inside useEffect. The correct location depends on what causes the operation.

Situation Recommended Approach
Connecting to a chat room because the selected room is displayed. useEffect
Updating the document title to match a changing prop. useEffect
Removing a browser event listener when it is no longer needed. Effect cleanup
Submitting a form after the user clicks Submit. Event handler
Purchasing a product after the user clicks Buy. Event handler
Deleting an item after the user confirms deletion. Event handler
Calculating a total from existing state values. Calculate during rendering

Example: Correctly Handling a Button Action

function BuyButton() {
  async function handleBuy() {
    const response = await fetch("/api/buy", {
      method: "POST"
    });

    if (!response.ok) {
      throw new Error("Purchase failed");
    }
  }

  return (
    <button onClick={handleBuy}>
      Buy Product
    </button>
  );
}

export default BuyButton;

The purchase is triggered by a specific user action, so the request belongs in the event handler rather than an Effect that runs when the component appears.


10. When You Do Not Need useEffect

A common mistake is using useEffect for work that React can handle directly during rendering or in an event handler.

Example: Unnecessary Effect for a Derived Value

import { useState, useEffect } from "react";

function PriceCalculator() {
  const [price, setPrice] = useState(100);
  const [quantity, setQuantity] = useState(2);
  const [total, setTotal] = useState(0);

  useEffect(() => {
    setTotal(price * quantity);
  }, [price, quantity]);

  return <p>Total: ₹{total}</p>;
}

This approach adds an extra state variable and an Effect for a value that can be calculated directly.

Better Approach

import { useState } from "react";

function PriceCalculator() {
  const [price, setPrice] = useState(100);
  const [quantity, setQuantity] = useState(2);

  const total = price * quantity;

  return (
    <div>
      <p>Total: ₹{total}</p>
      <button onClick={() => setPrice(p => p + 10)}>
        Increase Price
      </button>
      <button onClick={() => setQuantity(q => q + 1)}>
        Increase Quantity
      </button>
    </div>
  );
}

export default PriceCalculator;

Now total is calculated during rendering from the latest values of price and quantity. No Effect or extra state variable is needed.


11. Common Problems with Side Effects

11.1 Infinite Render Loops

An Effect can cause an infinite loop if it updates state on every run and that update causes the Effect to run again.

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

Without a dependency array, this Effect runs after every commit. Updating the count causes another render, which runs the Effect again.

Solution: Reconsider whether an Effect is needed. Specify correct dependencies and avoid setting state in an Effect when the value can be calculated during rendering.

11.2 Missing Dependencies

Leaving out reactive values used inside an Effect may cause it to work with outdated values.

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

If userName changes, the Effect above does not rerun to log the new value.

Solution: If the Effect should react to changes in userName, include it in the dependency array.

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

11.3 Forgetting Cleanup

Forgetting to clear timers or remove listeners can leave unwanted work running after a component is removed.

Solution: Return a cleanup function that releases the resources created by the Effect.

11.4 Duplicate Requests or Connections in Development

When Strict Mode is enabled, React performs an additional development-only Effect setup and cleanup cycle to help identify missing cleanup logic. This is expected behavior and does not mean that React always runs Effects twice in production.

Solution: Make Effects safe to set up, clean up, and set up again. Do not hide the issue with a ref that merely prevents a second setup from running.

11.5 Stale Asynchronous Results

An earlier request may finish after a newer request and overwrite the newer result.

Solution: Use cancellation, an ignore flag, or a data-fetching library that handles request ordering and caching.


12. Side Effects and React Strict Mode

React Strict Mode helps detect potential problems during development. With Strict Mode enabled, React runs an additional setup and cleanup cycle for Effects during initial mounting.

Example

import { useEffect } from "react";

function Example() {
  useEffect(() => {
    console.log("Effect setup");

    return () => {
      console.log("Effect cleanup");
    };
  }, []);

  return <h2>React Strict Mode</h2>;
}

export default Example;

When Strict Mode is enabled in development, you may see setup, cleanup, and setup logs. This helps test whether the Effect correctly releases resources and can safely run again.

  • Strict Mode is a development tool.
  • The extra Effect cycle helps uncover missing cleanup.
  • Do not rely on an Effect running only once as a way to guarantee a business operation happens once.
  • Actions such as purchases should be triggered by the appropriate user event and protected against duplicate submissions where necessary.

13. Side Effects vs useLayoutEffect

Both useEffect and useLayoutEffect are used for side effects, but their timing differs.

Feature useEffect useLayoutEffect
Timing Usually runs after the browser has had a chance to paint. Runs after DOM changes are committed but before browser repaint.
Common use External synchronization, subscriptions, and timers. Measuring layout and making visual corrections before paint.
Performance Usually preferable for ordinary effects. Can block painting, so use only when necessary.
Example Updating a connection when a room changes. Measuring a tooltip's dimensions before positioning it.

Best practice: Prefer useEffect unless the task requires work before the browser repaints.


14. Best Practices for Managing Side Effects

  • Keep rendering pure and avoid external mutations during rendering.
  • Use useEffect to synchronize with external systems.
  • Use event handlers for actions caused by specific user interactions.
  • Include all required reactive dependencies.
  • Clean up timers, subscriptions, listeners, and connections.
  • Handle asynchronous errors and stale results appropriately.
  • Avoid storing values in state when they can be calculated during rendering.
  • Do not use Effects simply to copy one state value into another.
  • Keep Strict Mode enabled during development to identify issues early.
  • Use data-fetching libraries or framework features when they better fit the application.

15. Interview Questions and Answers

Q1. What is a side effect in React?

Answer: A side effect is an operation that interacts with something outside the component's pure rendering calculation, such as a network request, timer, browser API, or external connection.

Q2. What is the purpose of useEffect?

Answer: useEffect allows a component to synchronize with external systems after React commits its changes.

Q3. What is the dependency array?

Answer: It specifies the reactive values that determine when an Effect should run again. React compares dependency values using Object.is.

Q4. Why is cleanup important?

Answer: Cleanup releases resources created by an Effect, such as intervals, event listeners, and external connections.

Q5. What happens when the dependency array is omitted?

Answer: The Effect runs after every commit.

Q6. What does an empty dependency array mean?

Answer: The Effect runs after the initial commit, with an additional development-only setup and cleanup cycle when applicable under Strict Mode.

Q7. Should every side effect be placed inside useEffect?

Answer: No. Side effects caused by a specific user action generally belong in event handlers. Effects are for synchronization caused by rendering.

Q8. What causes an infinite loop with useEffect?

Answer: An Effect can loop when it updates state, causing a render that triggers the Effect again, especially when dependencies are omitted or structured incorrectly.

Q9. What is the difference between useEffect and useLayoutEffect?

Answer: useEffect is suitable for most external synchronization, while useLayoutEffect runs before browser repaint and is useful for layout measurement or visual correction.

Q10. Why can an Effect run more than once in development?

Answer: Strict Mode performs an extra development-only setup and cleanup cycle to help reveal missing cleanup and other bugs.


16. Practice Exercises

  1. Create a component that updates the browser tab title when a counter changes.
  2. Build a timer using useEffect and clear the interval during cleanup.
  3. Listen for the browser's online event and remove the listener when the component is removed.
  4. Fetch a list of users from an API and display loading, success, and error states.
  5. Create a component that connects to a mock service and disconnects during cleanup.
  6. Fix an infinite loop caused by an Effect that updates state on every render.
  7. Calculate a shopping cart total without using an unnecessary Effect.
  8. Explain why a purchase request should run in a button event handler rather than on component mount.
  9. Test an Effect in Strict Mode and verify that its cleanup works correctly.
  10. Compare useEffect and useLayoutEffect with a practical example.

17. Quick Revision Checklist

  • Understand what side effects are.
  • Keep component rendering pure.
  • Know when to use useEffect.
  • Understand dependency arrays.
  • Know when and how to return a cleanup function.
  • Distinguish Effects from event handlers.
  • Avoid unnecessary Effects and derived state.
  • Handle asynchronous requests and stale results carefully.
  • Understand Strict Mode's development-only Effect cycle.
  • Know when useLayoutEffect is appropriate.

18. React JS Training Resources

Explore these resources to learn more about React Hooks, side effects, and modern React development.


19. Conclusion

Understanding side effects is essential for writing reliable React applications. Keep rendering pure, use event handlers for operations caused by specific user actions, and use useEffect when a component needs to synchronize with an external system. Manage dependencies carefully, clean up resources when necessary, and avoid Effects for calculations that can be performed directly during rendering. These practices help create React components that are predictable, maintainable, and efficient.

whatsapp