The dependency array is one of the most important concepts in React hooks - and one of the most confusing for beginners. It controls when your effects run, and getting it wrong leads to bugs, infinite loops, and stale data. In this lesson, you'll learn exactly how dependency arrays work, what to include, common mistakes to avoid, and how to use them to write efficient, bug-free React code. Let's master dependencies! šÆ
What Are Dependencies?
Dependencies are values that your effect depends on to work correctly:
function UserProfile({ userId }) {
const [user, setUser] = React.useState(null)
React.useEffect(() => {
// This effect DEPENDS ON userId
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => setUser(data))
}, [userId]) // userId is a dependency
return <div>{user?.name}</div>
}
// Why is userId a dependency?
// Because the effect uses userId to fetch the right user.
// If userId changes, effect needs to re-run with new value.The Dependency Rule:
Every value from the component scope that's used inside the effect must be in the dependency array. This includes:
⢠Props
⢠State variables
⢠Functions defined in the component
⢠Variables declared in the component
Three Forms of Dependency Arrays
1. No Array - Runs After Every Render
function Component() {
const [count, setCount] = React.useState(0)
const [name, setName] = React.useState('')
React.useEffect(() => {
console.log('Effect ran!')
document.title = `Count: ${count}, Name: ${name}`
}) // No dependency array
return (
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
<input value={name} onChange={e => setName(e.target.value)} />
</div>
)
}
// Effect runs:
// - After initial render
// - After EVERY re-render (any state/prop change)
// When to use:
// - When you need to sync with something on every render
// - Example: Updating document title based on current state
// Usually you want more control than this!2. Empty Array [] - Runs Once on Mount
function Component() {
const [data, setData] = React.useState(null)
React.useEffect(() => {
console.log('Effect ran!')
fetch('/api/initial-data')
.then(res => res.json())
.then(data => setData(data))
}, []) // Empty dependency array
return <div>{data?.name}</div>
}
// Effect runs:
// - After initial render
// - NEVER runs again!
// When to use:
// - Setup that should happen once (initialize, subscribe)
// - Fetching initial data that doesn't depend on props/state
// - Adding event listeners that don't change
// Common for "component did mount" behavior3. With Dependencies - Runs When Dependencies Change
function SearchResults({ query, sortBy }) {
const [results, setResults] = React.useState([])
React.useEffect(() => {
console.log('Effect ran!')
fetch(`/api/search?q=${query}&sort=${sortBy}`)
.then(res => res.json())
.then(data => setResults(data))
}, [query, sortBy]) // Dependencies
return (
<ul>
{results.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
)
}
// Effect runs:
// - After initial render
// - When query changes
// - When sortBy changes
// - NOT when other things change (other state/props)
// When to use:
// - When effect depends on specific values
// - Most common form!What to Include in Dependencies
ā DO Include These:
Props
function Component({ userId, filter }) {
React.useEffect(() => {
fetchUserData(userId, filter) // Uses props
}, [userId, filter]) // ā
Include both props
}State Variables
function Component() {
const [count, setCount] = React.useState(0)
const [multiplier, setMultiplier] = React.useState(2)
React.useEffect(() => {
const result = count * multiplier // Uses state
console.log(result)
}, [count, multiplier]) // ā
Include both state variables
}Functions Defined in Component
function Component({ userId }) {
const [user, setUser] = React.useState(null)
const fetchUser = () => {
fetch(`/api/users/${userId}`) // Uses userId prop
.then(res => res.json())
.then(data => setUser(data))
}
React.useEffect(() => {
fetchUser() // Calls function
}, [fetchUser]) // ā
Include function
// Problem: fetchUser is recreated every render!
// See "Functions as Dependencies" section below for solution
}ā DON'T Include These:
State Setters
function Component() {
const [count, setCount] = React.useState(0)
React.useEffect(() => {
setCount(prev => prev + 1)
}, []) // ā DON'T include setCount - it's stable
// State setters never change identity
// React guarantees they're stable across rendersConstants/Literals
const API_URL = 'https://api.example.com'
function Component() {
React.useEffect(() => {
fetch(API_URL)
}, []) // ā DON'T include API_URL - it never changes
// Constants defined outside component never changeRef Values
function Component() {
const ref = React.useRef(null)
React.useEffect(() => {
ref.current.focus()
}, []) // ā DON'T include ref - it's stable
// The ref object itself never changes
// (ref.current can change, but ref can't)Common Dependency Mistakes
1. Missing Dependencies - Stale Data
// ā Wrong - Missing dependency
function Component({ userId }) {
const [user, setUser] = React.useState(null)
React.useEffect(() => {
fetch(`/api/users/${userId}`) // Uses userId
.then(res => res.json())
.then(data => setUser(data))
}, []) // ā Missing userId!
return <div>{user?.name}</div>
}
// Problem:
// 1. Component renders with userId = 1, fetches user 1
// 2. userId prop changes to 2
// 3. Effect doesn't re-run (empty array)
// 4. Still showing user 1 data!
// ā
Correct - Include userId
React.useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => setUser(data))
}, [userId]) // ā
Include userId2. Object/Array Dependencies - Infinite Loop
// ā Wrong - Infinite loop!
function Component() {
const [user, setUser] = React.useState(null)
const options = { method: 'GET' } // New object every render!
React.useEffect(() => {
fetch('/api/user', options)
.then(res => res.json())
.then(data => setUser(data))
}, [options]) // ā options is always "different"!
}
// Problem:
// 1. Component renders, creates new options object
// 2. Effect runs because options changed
// 3. setUser causes re-render
// 4. New options object created
// 5. Effect runs again... infinite loop!
// ā
Solution 1: Move object inside effect
React.useEffect(() => {
const options = { method: 'GET' }
fetch('/api/user', options)
}, [])
// ā
Solution 2: Use useMemo (advanced)
const options = React.useMemo(() => ({ method: 'GET' }), [])
// ā
Solution 3: Move outside component
const OPTIONS = { method: 'GET' }
function Component() {
React.useEffect(() => {
fetch('/api/user', OPTIONS)
}, [])3. State in Dependencies - Infinite Loop
// ā Wrong - Infinite loop!
function Component() {
const [data, setData] = React.useState([])
React.useEffect(() => {
setData([...data, 'new item']) // Updates data
}, [data]) // ā data in dependencies!
}
// Problem:
// 1. Effect runs, updates data
// 2. data changes, effect runs again
// 3. data changes, effect runs again
// 4. Infinite loop!
// ā
Solution: Use functional update
React.useEffect(() => {
setData(prev => [...prev, 'new item'])
}, []) // Remove data from dependencies4. Functions as Dependencies
// ā Problem - Function recreated every render
function Component({ userId }) {
const fetchUser = () => {
fetch(`/api/users/${userId}`)
}
React.useEffect(() => {
fetchUser()
}, [fetchUser]) // fetchUser is new every render!
}
// Problem: fetchUser is a new function every render,
// so effect runs every render
// ā
Solution 1: Move function inside effect
React.useEffect(() => {
const fetchUser = () => {
fetch(`/api/users/${userId}`)
}
fetchUser()
}, [userId])
// ā
Solution 2: Use useCallback (advanced)
const fetchUser = React.useCallback(() => {
fetch(`/api/users/${userId}`)
}, [userId])
React.useEffect(() => {
fetchUser()
}, [fetchUser])Interactive Demo
š” Tip: Edit the code above and click "Run" to see your changes
ESLint Plugin for Dependencies
React has an ESLint plugin that warns about dependency mistakes:
# Install the plugin
npm install eslint-plugin-react-hooks --save-dev
# Add to .eslintrc.js
{
"plugins": ["react-hooks"],
"rules": {
"react-hooks/exhaustive-deps": "warn"
}
}The plugin will warn you when:
- You're missing dependencies
- You have unnecessary dependencies
- You're creating infinite loops
ā Trust the Linter
The exhaustive-deps rule is very smart!
When it warns about missing dependencies, it's usually right.
Don't disable the warning without understanding why!
Missing dependencies lead to bugs with stale data.
Optimization Patterns
Pattern 1: Extract Static Values
// ā Before - Object created every render
function Component() {
const options = { method: 'GET', headers: {} }
React.useEffect(() => {
fetch('/api/data', options)
}, [options]) // New object every render!
}
// ā
After - Move outside component
const OPTIONS = { method: 'GET', headers: {} }
function Component() {
React.useEffect(() => {
fetch('/api/data', OPTIONS)
}, []) // OPTIONS never changes
}Pattern 2: Move Logic Inside Effect
// ā Before - Function as dependency
function Component({ userId }) {
const fetchUser = () => fetch(`/api/users/${userId}`)
React.useEffect(() => {
fetchUser()
}, [fetchUser]) // fetchUser is new every render
}
// ā
After - Move inside effect
function Component({ userId }) {
React.useEffect(() => {
const fetchUser = () => fetch(`/api/users/${userId}`)
fetchUser()
}, [userId]) // Only userId as dependency
}Pattern 3: Use Functional Updates
// ā Before - State as dependency
function Component() {
const [count, setCount] = React.useState(0)
React.useEffect(() => {
const id = setInterval(() => {
setCount(count + 1) // Uses count
}, 1000)
return () => clearInterval(id)
}, [count]) // Re-runs every second!
}
// ā
After - Functional update
function Component() {
const [count, setCount] = React.useState(0)
React.useEffect(() => {
const id = setInterval(() => {
setCount(prev => prev + 1) // Doesn't use count
}, 1000)
return () => clearInterval(id)
}, []) // Runs once!
}Key Takeaways
- Dependency array controls when effects run
- No array = runs after every render
- Empty [] = runs once on mount
- [dependencies] = runs when dependencies change
- Include all values used in effect from component scope
- Don't include: setState functions, refs, constants
- Missing dependencies cause stale data bugs
- Object/array dependencies can cause infinite loops
- Use functional updates to avoid state dependencies
- Move functions inside effects when possible
- Extract static values outside component
- Use ESLint plugin to catch dependency mistakes
- React compares dependencies with
Object.is()
What's Next?
Congratulations! You've completed the Side Effects and Data section. You now understand:
- What side effects are and how to handle them
- How to fetch data from APIs with useEffect
- How to clean up effects to prevent memory leaks
- How to use dependency arrays to control effect execution
You have the foundation for building data-driven React applications that interact with APIs and external systems! But there's still more to learn:
- Performance Optimization - useMemo, useCallback, React.memo
- Custom Hooks - Creating reusable logic
- Context API - Sharing state across components
- Advanced Patterns - Composition, render props
- And much more!
Keep building - you're becoming a React expert! š