Understanding useEffect: No dependencies vs empty array vs dependencies array


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
changesWhenever
category
changesNot 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
Pattern | Syntax | Runs When | Example Use Cases |
No dependencies | useEffect(() => {}) | • 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 dependencies | useEffect(() => {}, []) | • Only on initial render • Never again until unmount | • Data fetching on mount • Setting up subscriptions • Component initialization • Event listeners setup |
With dependencies | useEffect(() => {}, [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
Pattern | Performance | When to Avoid |
No dependencies | ⚠️ High overhead | Components that re-render frequently |
Empty dependencies | ✅ Most efficient | When you need to react to changes |
With dependencies | ✅ Efficient | Never - this is the recommended pattern |
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