Skip to main content

The Dependency Array Explained (Part 1) #67

📖 Introduction

In our previous explorations of useEffect, we learned how to perform side effects like fetching data when a component mounts. However, the true power of useEffect lies in its ability to precisely control when an effect runs. This control is managed by its second argument: the dependency array.

Understanding the dependency array is not just important; it is the key to mastering useEffect and preventing common bugs and performance issues. This article will break down the three fundamental ways to use it.


📚 Prerequisites

Before you proceed, ensure you are comfortable with:

  • The basic syntax and purpose of the useEffect hook.
  • The concept of component rendering and re-rendering in React.

🎯 Article Outline: What You'll Master

By the end of this article, you will understand the three core behaviors of the useEffect dependency array:

  • No Array: How to make an effect run after every single render.
  • Empty Array ([]): How to make an effect run only once, when the component mounts.
  • Array with Dependencies ([dep1, dep2, ...]): How to run an effect in response to changes in specific props or state.

🧠 Section 1: The Three Modes of useEffect

The dependency array is the second argument you pass to useEffect. It's an array of variables (props or state) that the effect "depends" on. How you define this array dictates when the effect will be re-executed.

There are three rules:

  1. If you don't provide an array, the effect runs after every render.
  2. If you provide an empty array ([]), the effect runs only once (after the initial render).
  3. If you provide an array with values ([prop, state]), the effect runs on mount AND whenever any of those values change.

Let's explore each with a clear example.


💻 Section 2: Scenario 1 - No Dependency Array

When you don't pass a second argument, the effect will run after the initial render and after every subsequent re-render.

Use Case: This is rarely what you want in practice, as it can cause performance issues. It's most often used for logging or debugging to observe how a component is rendering.

// EffectOnEveryRender.jsx

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

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

useEffect(() => {
console.log(`Effect ran! The current count is: ${count}`);
// Notice there is no second argument array.
});

return (
<div>
<p>Open the console to see the effect running.</p>
<button onClick={() => setCount(c => c + 1)}>
Increment Count ({count})
</button>
</div>
);
}

Result: When you run this code, you will see the log message on the initial render and then again every single time you click the button.


🛠️ Section 3: Scenario 2 - The Empty Dependency Array: []

This is the pattern we used in the previous article to fetch data. By providing an empty array, you are telling React, "This effect doesn't depend on any props or state, so it never needs to re-run."

Use Case: This is perfect for setup code that only needs to run once, such as:

  • Fetching initial data for a component.
  • Setting up timers or event listeners that should last for the component's entire lifecycle.
// FetchOnMount.jsx

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

function FetchOnMount() {
const [data, setData] = useState(null);

useEffect(() => {
console.log('Effect ran! Fetching data...');
// In a real app, you would fetch data here.
// We'll simulate it with a timeout.
setTimeout(() => setData({ message: 'Data has been fetched!' }), 2000);
}, []); // The empty array `[]` is the key.

return (
<div>
{data ? <p>{data.message}</p> : <p>Loading...</p>}
</div>
);
}

Result: The "Effect ran!" message will appear in the console only once, right after the component first renders.


🚀 Section 4: Scenario 3 - The Dependency Array with Values

This is the most powerful and common use case. You provide an array of variables, and React will re-run the effect whenever any of those variables change.

Use Case: Synchronizing with an external system based on the current state of your app.

  • Re-fetching data when a user ID changes.
  • Updating a chart when a new data point is added.
  • Resetting a timer when a specific prop changes.

Let's build a component that fetches a user's data based on their ID. When the ID changes, it should fetch the new user's data.

// UserDataFetcher.jsx

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

function UserDataFetcher({ userId }) {
const [user, setUser] = useState(null);

useEffect(() => {
console.log(`Effect ran! Fetching data for user ${userId}`);
// Fetch data for the specific userId
fetch(`https://jsonplaceholder.typicode.com/users/${userId}`)
.then(response => response.json())
.then(data => setUser(data));
}, [userId]); // The effect now depends on `userId`.

if (!user) {
return <div>Loading user data...</div>;
}

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

// A parent component to control the userId
export default function UserSwitcher() {
const [currentUserId, setCurrentUserId] = useState(1);

return (
<div>
<button onClick={() => setCurrentUserId(1)}>User 1</button>
<button onClick={() => setCurrentUserId(2)}>User 2</button>
<button onClick={() => setCurrentUserId(3)}>User 3</button>
<UserDataFetcher userId={currentUserId} />
</div>
)
}

Result: The effect will run when the component first mounts (for user 1). Then, every time you click a button to change the currentUserId, the userId prop in UserDataFetcher changes, and React will re-run the effect to fetch the new user's data.


💡 Conclusion & Key Takeaways

You now understand the fundamental mechanism for controlling side effects in React. The dependency array gives you the power to be precise and efficient.

Let's summarize the three rules:

  • No Array: Runs on every render. (Use sparingly).
  • Empty Array []: Runs only once on mount. (For initial setup).
  • Dependency Array [dep]: Runs on mount and when a dependency changes. (For syncing with state/props).

Getting the dependency array right is crucial for writing correct and performant React code.


➡️ Next Steps

While the dependency array is powerful, it also comes with its own set of rules and potential pitfalls. What happens if you forget a dependency? What if you include a function or an object? In the next article, "The Dependency Array Explained (Part 2)", we will explore these common challenges and learn how to solve them to become true masters of the useEffect hook.

Your control over React's lifecycle is growing. Let's solidify that control.