Understanding useEffect: No dependencies vs empty array vs dependencies array

Gilles FerrandGilles Ferrand
5 min read

The useEffect hook is fundamental to React functional components, but its behaviour changes dramatically based on how you handle the dependencies array. Let's explore the three main patterns and when to use each one.

useEffect without dependencies array

// ComponentA.tsx
import { useEffect } from 'react';

const ComponentA = () => {
  useEffect(() => {
    console.log('Component A mounted');
    return () => {
      // NOTE: Cleanup function, we will see this in a future blog post
      console.log('Component A unmounted');
    };
  });

  return <p>Component A works</p>;
};

export default ComponentA;
// App.tsx
import { useState } from 'react';
import ComponentA from './ComponentA';

function App() {
  const [isComponentAVisible, setComponentAVisible] = useState(true);
  const [count, setCounter] = useState(0);

  return (
    <>
      <p>App works</p>
      <p>Counter: {count}</p>
      <button onClick={() => setCounter((prev) => prev + 1)}>
        Increment Counter
      </button>
      <button onClick={() => setComponentAVisible(!isComponentAVisible)}>
        {isComponentAVisible ? 'Hide' : 'Show'} component A
      </button>
      {isComponentAVisible && <ComponentA />}
    </>
  );
}

export default App;

Console output :

// Component A mounted (page loads)
// Component A mounted (page loads / Only if strict mode is activated)
// Component A unmounted (hide component A clicked)
// Component A mounted (show component A clicked)
// Component A unmounted (Only if strict mode is activated)
// Component A mounted (Only if strict mode is activated)

In this case useEffects run :

  • Initial render

  • Every state update

  • Every prop changed

  • Every parent re-render

Use case : Rare. Typically only when you need to respond to any component change, such as logging or analytics tracking.

useEffect without empty dependencies array

An empty dependencies array makes the effect run only once after the initial render.

// ComponentA.tsx
import { useEffect } from 'react';

const ComponentA = () => {
  useEffect(() => {
    console.log('Component A mounted');
    return () => {
      // NOTE: Cleanup function, we will see this in a future blog post
      console.log('Component A unmounted');
    };
  }, []);

  return <p>Component A works</p>;
};

export default ComponentA;

Console output :

// Component A mounted (page loads)
// Component A mounted (page loads / Only if strict mode is activated)

// Clicking on increment counter doesn't log anything

// Component A unmounted (hide component A clicked)
// Component A mounted (show component A clicked)
// Component A unmounted (Only if strict mode is activated)
// Component A mounted (Only if strict mode is activated)

In this case useEffects run :

  • Only after the initial render

  • Never again until component unmounts

Use case : Component initialization, data fetching that happens once, setting up subscriptions, or DOM manipulations that should only happen on mount.

useEffect with dependencies array

When you specify dependencies, the effect runs after the initial render and whenever any dependency changes.

// ComponentA.tsx
import { useEffect } from 'react';

interface ComponentPropsA {
  readonly userId: number;
  readonly theme: 'light' | 'dark';
}

const ComponentA = ({ userId, theme }: ComponentPropsA) => {
  useEffect(() => {
    console.log(
      `Component A mounted with userId: ${userId} and theme: ${theme}`
    );

    // Example: fetch user-specific data or apply theme
    if (userId) {
      console.log(`Fetching data for user ${userId} with ${theme} theme`);
    }

    return () => {
      // NOTE: Cleanup function, we will see this in a future blog post
      console.log(`Component A unmounted with userId: deps changed`);
    };
  }, [userId, theme]); // Effect depends on userId and theme

  return <p>Component A works</p>;
};

export default ComponentA;
// App.tsx
import { useState } from 'react';
import ComponentA from './ComponentA';

function App() {
  const [isComponentAVisible, setComponentAVisible] = useState(true);
  const [userId, setUserId] = useState(1);
  const [theme, setTheme] = useState<'light' | 'dark'>('light');
  const [count, setCount] = useState(0);

  return (
    <>
      <p>App works</p>
      <p>Counter: {count}</p>
      <button onClick={() => setCount((prev) => prev + 1)}>
        Increment Counter
      </button>
      <button onClick={() => setUserId((prev) => prev + 1)}>
        Increment User ID
      </button>
      <button
        onClick={() =>
          setTheme((prev) => (prev === 'light' ? 'dark' : 'light'))
        }
      >
        Toggle Theme
      </button>
      <button onClick={() => setComponentAVisible(!isComponentAVisible)}>
        {isComponentAVisible ? 'Hide' : 'Show'} component A
      </button>
      {isComponentAVisible && <ComponentA userId={userId} theme={theme} />}
    </>
  );
}

export default App;

Console output :

// Component A mounted with userId: 1 and theme: light (page loads)
// Fetching data for user 1 with light theme (page loads)
// Component A unmounted with userId: deps changed (page loads / Only if strict mode is activated)
// Component A mounted with userId: 1 and theme: light (page loads / Only if strict mode is activated)
// Fetching data for user 1 with light theme (page loads / Only if strict mode is activated)

// Clicking on increment counter doesn't log anything

// Clicking on increment userId
// Component A unmounted with userId: deps changed
// Component A mounted with userId: 2 and theme: light
// Fetching data for user 2 with light theme

// Clicking on toggle theme
// Component A unmounted with userId: deps changed
// Component A mounted with userId: 2 and theme: dark
// Fetching data for user 2 with dark theme

In this case useEffects run :

  • After the initial render

  • Whenever query changes

  • Whenever category changes

  • Not when other state variables change

Use case : Responding to specific state or prop changes, such as fetching data based on user input, updating derived state, or synchronizing with external systems.

Quick Comparison Table

PatternSyntaxRuns WhenExample Use Cases
No dependenciesuseEffect(() => {})• Initial render
• Every state update
• Every prop change
• Every parent re-render
• Logging/analytics
• DOM updates that need to happen on every render
• Performance monitoring
Empty dependenciesuseEffect(() => {}, [])• Only on initial render
• Never again until unmount
• Data fetching on mount
• Setting up subscriptions
• Component initialization
• Event listeners setup
With dependenciesuseEffect(() => {}, [a, b])• Initial render
• When any dependency changes
• Not when other state changes
• Reactive data fetching
• Responding to prop changes
• Updating derived state
• Search/filter operations

Performance Impact

PatternPerformanceWhen to Avoid
No dependencies⚠️ High overheadComponents that re-render frequently
Empty dependenciesMost efficientWhen you need to react to changes
With dependenciesEfficientNever - this is the recommended pattern
0
Subscribe to my newsletter

Read articles from Gilles Ferrand directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Gilles Ferrand
Gilles Ferrand

Full stack engineer but passionnated by front-end Angular Expert / NX / JavaScript / Node / Redux State management / Rxjs