Hello! Welcome to our next lesson on React hooks.
In our last session, we established a foundational pattern: using useEffect with an empty dependency array ([]) to fetch data from an API exactly once, when a component first mounts. This is perfect for loading initial page data.
Today, we'll build on that knowledge to make our effects more dynamic. Often, you need a side effect to re-run in response to a change, like a user typing in a search box or selecting a filter. This lesson is all about mastering that control. Your goal is to use the dependency array to control when an effect re-runs. By the end of this lesson, you'll understand how to "subscribe" your effect to specific state or prop values, making your components truly interactive.
1. The Dependency Array: A "Watchlist" for React
The second argument to useEffect is called the dependency array. You can think of it as a "watchlist" you give to React. After every render, React looks at the items on this list and compares their current values to their values from the previous render.
- If any value on the list has changed, React will run your effect.
- If no values on the list have changed, React will skip the effect.
This gives us precise control. Let's explore the three main ways you can use this array.
React useEffect Dependency Arrays: Preventing Infinite Loops and ...
To start, let's get a clear, high-level overview of how the dependency array works. This article breaks it down into three distinct modes of operation.
Please read the short section titled '🔄 The Three Modes of Dependency Arrays'. This will quickly summarize the three configurations we'll be discussing.
To summarize and expand on what you just read:
useEffect(callback, [])(Empty Array): You're telling React, "My effect's watchlist is empty; it doesn't depend on anything." React runs it once after the first render and never again. We used this in the last lesson.useEffect(callback)(No Array): You're telling React, "There is no watchlist, so just run the effect all the time." The effect will run after every single render of the component. This is rarely what you want and can easily cause performance problems or infinite loops.useEffect(callback, [dep1, dep2])(Array with Dependencies): This is the focus of today's lesson. You're telling React, "Watchdep1anddep2. If either of them changes between renders, re-run my effect."
2. A Practical Example: Re-fetching Data on Input Change
Let's apply this to a common scenario. Imagine you have a search input. You want to fetch new data from your API every time the user types a new search query.
Here’s how you would structure the component:
import React, { useState, useEffect } from 'react';
function SearchableList() {
const [query, setQuery] = useState(''); // State for the search input
const [items, setItems] = useState([]); // State for the API results
// This effect will re-run whenever the 'query' state changes.
useEffect(() => {
// We don't want to search for an empty string
if (query === '') {
setItems([]); // Clear results if query is empty
return;
}
console.log(`Fetching data for: ${query}`);
fetch(`https://api.example.com/search?q=${query}`)
.then(response => response.json())
.then(data => {
setItems(data.results);
});
}, [query]); // <-- The magic is here!
return (
<div>
<input
type="text"
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Search..."
/>
<ul>
{items.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
</div>
);
}
Here is the flow:
- The component renders.
queryis''. The effect runs, sees the empty query, and does nothing. - The user types "Laravel" into the input.
setQuery('Laravel')is called. - This state change triggers a re-render.
- After the re-render, React compares the new
query('Laravel') with the old one (''). It sees they are different. - Because
queryis in the dependency array, React re-runs the effect. Thefetchis made with the new query. - The
itemsstate is updated, and the new results are displayed.
The following resource illustrates this exact scenario.
React useEffect Dependency Arrays: Preventing Infinite Loops and ...
This is such a common pattern that it's worth seeing another clear example. The article you read earlier has a perfect walkthrough of debugging a component where the fetch isn't re-running and fixing it with the dependency array.
Read the section '🧪 Let’s Debug a Real Problem Together'. It shows code where a query dependency is missing and then adds it to fix the bug. This directly mirrors our example above.
3. Common Pitfalls and Best Practices
Using the dependency array correctly is crucial. Misusing it can lead to two major types of bugs: infinite loops and stale data.
Pitfall 1: The Infinite Loop
An infinite loop occurs when an effect modifies a state variable that is also in its own dependency array.
React useEffect Dependency Arrays: Preventing Infinite Loops and ...
Let's examine how an infinite loop happens. It's a classic mistake, and understanding the cause-and-effect cycle is the best way to prevent it.
Read the section '💥 Infinite Loops: How They Happen (and Why)'. The step-by-step breakdown clearly explains the feedback loop created by the effect.
As you read, the cycle is:
- Effect runs.
- It calls a
setStatefunction. - The state change causes a re-render.
- On re-render, the state value has changed.
- Since that state is a dependency, the effect runs again.
- Go back to step 2. Repeat forever. 🔥
Pitfall 2: Stale Data and the Linter Rule
The opposite problem is when your effect doesn't run when it should. This happens if you use a prop or state variable inside your effect but forget to add it to the dependency array.
The effect creates a "closure" over the props and state from the render it was defined in. If those values change on a later render, the effect won't see the new values unless it's told to re-run by the dependency array. It will be working with stale data.
Luckily, the React team provides an ESLint plugin (eslint-plugin-react-hooks) that automatically detects this. If you are using a tool like Create React App or Vite, this is enabled by default. It will give you a warning like: React Hook useEffect has a missing dependency: 'someValue'.
You should almost always trust this warning. Don't disable it. Instead, add the missing dependency. If adding it causes an infinite loop, it means your effect has a structural problem that needs to be refactored.
React useEffect Dependency Arrays: Preventing Infinite Loops and ...
Let's explore the 'stale data' bug and why the linter rule is so important. This article explains the problem and the right way to think about the linter's warnings.
First, read the part under '❗ What Happens If You Forget a Dependency?'. Then, read the section '🔍 ESLint’s exhaustive-deps Rule' to understand why it's a tool to help you, not an annoyance to be silenced.
Conclusion
You've now unlocked the full power of useEffect. By managing the dependency array, you can subscribe to changes in your component's props and state, allowing you to run side effects at precisely the right moments.
Key Takeaways:
- The dependency array tells React when to re-run an effect by watching for changes in specified values.
- Empty array
[]: Runs once on mount. - No array: Runs after every render (use with caution).
- Array with values
[dep1]: Runs on mount and whenever any dependency in the array changes. - Always include all props and state values that your effect reads inside the dependency array.
- Trust the
exhaustive-depsESLint rule. It helps you prevent bugs related to stale data. - Be wary of creating infinite loops by having an effect update a state variable that it also depends on.
Preview of the Next Lesson:
Our data-fetching logic is getting more robust. We can fetch data on mount and re-fetch it when a query changes. But what happens while the fetch is in progress? The user just sees a blank space. And what if the network request fails? In our next lesson, we will learn how to implement loading and error states for our asynchronous operations, creating a much more professional and user-friendly experience.