React useEffect Hook

  • useEffect is a fundamental React hook that allows you to synchronize a component with an external system. This includes tasks like fetching data, manually changing the DOM, or setting up subscriptions.
  • In the past, class components used lifecycle methods like componentDidMount, componentDidUpdate, and componentWillUnmount. The useEffect hook consolidates these into a single, more expressive API.
Developer Tip: Think of useEffect as a way to "step outside" of the React rendering flow to interact with the outside world, such as an API or a browser global like window.

Purpose

  • External Synchronization: It handles asynchronous operations like API calls or WebSocket connections that shouldn't block the browser from painting the UI.
  • Lifecycle Management: It provides a predictable way to manage when code should run based on the component's lifecycle or state changes.
  • Resource Management: It ensures that resources (like event listeners or timers) are properly cleaned up to prevent memory leaks in your application.

Syntax

  • Import useEffect from the 'react' library.
  • The first argument is the effect function containing your logic.
  • The second argument is the dependency array, which tells React exactly when to re-run the effect.
import React, { useEffect } from 'react';

useEffect(() => {
  // Your side effect logic goes here
  
  return () => {
    // Optional: Cleanup code runs before the effect re-runs and on unmount
  };
}, [dependencies]); // The effect re-runs whenever any value in this array changes
Best Practice: Always declare the dependency array, even if it is empty. This makes your intent clear to other developers and prevents unintended behavior.

Runs on every render

If you omit the dependency array entirely, useEffect will execute after every single render of the component. This is rarely what you want, as it can lead to significant performance bottlenecks.

import React, { useEffect, useState } from 'react';

const EveryRenderExample = () => {
  const [text, setText] = useState("");

  useEffect(() => {
    // This runs every time the component updates
    console.log('Effect ran because the component re-rendered!');
  });

  return (
    <div>
      <input value={text} onChange={(e) => setText(e.target.value)} />
      <p>Check the console to see the effect firing on every keystroke.</p>
    </div>
  );
};

export default EveryRenderExample;
Common Mistake: Forgetting the dependency array when performing a state update inside the effect. This will trigger a re-render, which triggers the effect, creating an infinite loop that can crash your browser.

Runs on first render

By providing an empty dependency array [], you tell React that this effect does not depend on any values from props or state. Consequently, it only runs once: immediately after the component is first added to the DOM (mounted).

import React, { useEffect, useState } from 'react';

const FirstRenderExample = () => {
  const [data, setData] = useState(null);

  useEffect(() => {
    // Perfect for initial data fetching
    fetch('https://api.example.com/items')
      .then(res => res.json())
      .then(json => setData(json));
    
    console.log('Component mounted. Fetching data...');
  }, []); // Empty array = run once on mount

  return (
    <div>
      <p>Data: {data ? JSON.stringify(data) : 'Loading...'}</p>
    </div>
  );
};

export default FirstRenderExample;

Runs when data changes

When you include variables in the dependency array, React performs a "shallow comparison." If any value in the array has changed since the last render, the effect runs again. This is ideal for responding to user input or prop updates.

import React, { useEffect, useState } from 'react';

const DataChangeExample = ({ userId }) => {
  const [user, setUser] = useState(null);

  useEffect(() => {
    // This effect runs on mount AND whenever userId changes
    const fetchUser = async () => {
      const response = await fetch(`https://api.example.com/users/${userId}`);
      const data = await response.json();
      setUser(data);
    };

    fetchUser();
  }, [userId]); // Only re-run if userId changes

  return (
    <div>
      <h1>Profile: {user?.name}</h1>
    </div>
  );
};

export default DataChangeExample;
Watch Out: If you pass an object or an array as a dependency, React compares them by reference, not value. If the object is recreated on every render, the effect will run every time.

Cleanup

Some side effects require "cleanup" to avoid memory leaks. Examples include closing a WebSocket, clearing a setTimeout, or removing a global event listener. You perform cleanup by returning a function from your effect.

import React, { useEffect, useState } from 'react';

const WindowWidthTracker = () => {
  const [width, setWidth] = useState(window.innerWidth);

  useEffect(() => {
    const handleResize = () => setWidth(window.innerWidth);
    
    // Set up the listener
    window.addEventListener('resize', handleResize);

    // Return the cleanup function
    return () => {
      // This runs before the component unmounts
      window.removeEventListener('resize', handleResize);
    };
  }, []); 

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

Dependency Array

  • The dependency array is the "brain" of the useEffect hook. It controls the execution frequency.
  • No array: Runs after every render.
  • Empty array []: Runs only once (on mount).
  • Array with values [prop, state]: Runs on mount and whenever prop or state changes.
Best Practice: Use the eslint-plugin-react-hooks. It will automatically warn you if you forget to include a variable in your dependency array that is used inside the effect.

Effect without Cleanup

Many side effects, like logging to the console or one-off API calls, don't require any cleanup. In these cases, you simply don't return anything from the effect function.

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // Updates the tab title whenever count changes

 

Summary

useEffect is a versatile tool that bridges the gap between React's declarative UI and the imperative nature of external APIs. By mastering the dependency array and the cleanup function, you can create highly performant and bug-free React applications that interact seamlessly with the real world.