Mastering useEffect Dependencies: Control Your React Lifecycle
Learn to master the useEffect dependency array to control exactly when your side effects run. Avoid infinite loops and optimize your React components today.
Previously in this course, we covered the Introduction to Side Effects: Managing External Logic in React, where we established that side effects—like fetching data or manually updating the DOM—should live outside the main render logic. Today, we add the "when" to the "what" by mastering the useEffect dependency array.
In React, the useEffect hook is your primary tool for syncing your component with the outside world. However, if you don't control when that sync happens, you'll quickly run into performance bottlenecks or, worse, infinite re-render loops.
Understanding the Dependency Array
The useEffect hook takes two arguments: a callback function (the effect) and an optional dependency array. The dependency array acts as a gatekeeper; it tells React, "Only run this effect if these specific values have changed since the last render."
If you omit the array entirely, the effect runs after every single render. This is usually not what you want.
1. Running only on mount
To run an effect exactly once—when the component first appears on the screen—you provide an empty dependency array: [].
JAVASCRIPTimport { useEffect } from CE9178">'react'; function MovieLogger() { useEffect(() => { console.log("Component mounted! This runs only once."); }, []); // Empty array = mount only return <div>Check the console!</div>; }
2. Triggering on state changes
If you want the effect to react to specific data, you list those variables inside the array. React compares the value from the previous render to the current render. If they differ, the effect fires.
In our movie-browser app, imagine we want to log the current search query every time the user types:
JAVASCRIPTimport { useState, useEffect } from CE9178">'react'; function SearchLogger({ query }) { useEffect(() => { console.log("Search query changed to:", query); }, [query]); // Runs whenever CE9178">'query' changes return <div>Searching for: {query}</div>; }
Worked Example: Synchronizing a Document Title
Let’s advance our project. We want the browser's tab title to update whenever the user changes their search query, so they always know what they are looking for.
JSXimport { useState, useEffect } from CE9178">'react'; export default function MovieBrowser() { const [query, setQuery] = useState(CE9178">''); // Sync document title with the search state useEffect(() => { document.title = query ? CE9178">`Searching for: ${query}` : CE9178">'Movie Browser'; }, [query]); // Dependency: re-run only when CE9178">'query' changes return ( <input value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Search movies..." /> ); }
In this example, the effect is "reactive." It doesn't matter how many times the MovieBrowser component re-renders for other reasons; the document.title logic only executes if query has a new value.
Hands-on Exercise
- Open your current movie-browser project.
- Inside your main component, create a
useEffectthat logs a message to the console saying "Effect triggered!" - Add a dependency array to that effect.
- Add a
console.log("Rendering component...")at the top level of your component function. - Observe how the logs behave when you update a state variable that is not in the dependency array versus one that is.
Common Pitfalls
- Lying to React: The biggest mistake is omitting a variable from the dependency array that you use inside the effect. React expects the array to be a complete list of dependencies. If you use a prop or state inside the effect but don't list it in the array, the effect will use "stale" (outdated) values.
- Infinite Loops: If you update a state variable inside an effect that also depends on that same variable, you create an infinite loop. The effect runs, updates state, which triggers a re-render, which triggers the effect, and so on.
- Overusing useEffect: Don't use
useEffectfor data that can be calculated during render. If you're just transforming data (like sorting a list), do it directly in the component body instead of syncing it to state via an effect. Read more on why you should stop using useEffect for data calculations to keep your components fast.
Recap
- The dependency array controls the execution frequency of
useEffect. - An empty array
[]ensures the effect runs only once on mount. - Listing variables in the array ensures the effect responds only to specific changes.
- Always include every reactive value used inside the effect to avoid stale data bugs.
Up next: We'll take these concepts further by learning how to fetch live movie data from an API and handle the asynchronous nature of network requests.
Work with me

Headless WordPress + Next.js Frontend Development
Keep WordPress for content, get a lightning-fast Next.js frontend. The best of both worlds — familiar editing, modern speed.

React & Next.js Dashboard / Admin UI Development
A clean, data-rich dashboard UI in React or Next.js — charts, tables, and real-time data that your users will actually enjoy using.