React: useEffect and the Lifecycle
Last updated: 2026-08-26
If
useStateis a component’s “memory,” thenuseEffectis its “camera”—it lets you do some extra things after the component has been rendered to the screen (“after taking the photo”): requisição data, subscribe to events, manipulate the DOM, and set timers.
1. What You'll Learn
- The Purpose and Basic Usage of useEffect
- Three Patterns for Using Arrays
- Cleanup functions (to prevent memory leaks)
- The Relationship Between
useEffectand the Component Lifecycle - useEffect vs. useLayoutEffect
2. The Story of a Data-Loading Page
(1) Pain Point: Data requests cause an infinite laço
Alice wants to requisição user data when the component loads, so she writes the following:
function UserProfile({ userId }) {
const [user, setUser] = React.useState(null)
// ❌ Error:Requesting data directly within a component's function body
fetch('/api/users/' + userId)
.then(res => res.json())
.then(data => setUser(data))
return <div>{user?.name}</div>
}
Question:
fetch→setUser(data)→ Component re-render- Render again → Execute
fetchagain → Set the status again → Render again... - Infinite loop! The page is frozen.
Alice needs a way to "execute only once when the component is mounted."
(2) useEffect solution
import { useState, useEffect } from 'react'
function UserProfile({ userId }) {
const [user, setUser] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
// ✅ Correct:useEffect + Empty array of dependencies
useEffect(() => {
fetch('/api/users/' + userId)
.then(res => {
if (!res.ok) throw new Error('Failed to load')
return res.json()
})
.then(data => {
setUser(data)
setLoading(false)
})
.catch(err => {
setError(err.message)
setLoading(false)
})
}, [userId]) // Only at userId Re-execute when changes occur
if (loading) return <p>Loading......</p>
if (error) return <p>Error:{error}</p>
return <h1>{user.name}</h1>
}
Benefits: useEffect Separates side effects from rendering logic—rendering is responsible only for the UI, and side effects are executed after rendering is complete.
3. useEffect basics
(1) Basic Syntax
useEffect(() => {
// Side Effect Logic(Execute after rendering is complete)
console.log('Component rendering complete!')
return () => {
// Cleanup Function(Run before unloading or re-executing a component)
console.log('Addressing Side Effects')
}
}, [Dependency Array])
| Section | Description |
|---|---|
() => { ... } |
Side-effect retorno de chamada, executed after rendering is committed to the screen |
return () => { ... } |
Cleanup function; runs before re-execution due to component uninstallation or dependency changes |
[dependencies] |
An array that controls when useEffect is re-run |
graph TB
A[Component Rendering] --> B[React Submit DOM Update]
B --> C[useEffect Callback Execution]
C --> D{Changes in Dependencies?}
D -->|is | E[Execute the previous cleanup function]
E --> F[Re-execute useEffect]
D -->|No| G[Skip]
F --> H{Component Uninstallation?}
G --> H
H -->|is | I[Execute the cleanup function]
style C fill:#61dafb,color:#000
style I fill:#ff6b6b,color:#fff
(2) Three Patterns for Dependency Arrays
// Pattern 1:Empty array [] — Execute only once when mounted
useEffect(() => {
console.log('Component Mounting')
fetch('/api/init').then(setData)
}, []) // Never re-execute
// Pattern 2:Dependencies [a, b] — Re-execute when dependencies change
useEffect(() => {
console.log('userId It has changed.:', userId)
fetch('/api/users/' + userId).then(setUser)
}, [userId]) // userId Re-execute when changes occur
// Pattern 3:No array dependencies — Executed on every render
useEffect(() => {
console.log('Executed on every render')
}) // Note:No array!
| Array of Dependencies | Execution Timing | Use Cases |
|---|---|---|
[] |
Once only upon mounting | Initialize data requests and subscriptions |
[dep1, dep2] |
When mounted + when dependencies change | Data changes with parameters |
| Countless sets | After each render | Rarely used (usually replaced by other methods) |
4. Cleanup Functions
Cleanup functions are key to preventing memory leaks and race conditions.
(1) Clearing the Timer
function Timer() {
const [seconds, setSeconds] = useState(0)
useEffect(() => {
const timer = setInterval(() => {
setSeconds(s => s + 1)
}, 1000)
// ✅ Clear the timer when the component is unloaded
return () => {
clearInterval(timer)
console.log('The timer has been cleared.')
}
}, []) // Start only when mounted
return <p>Running: {seconds}s</p>
}
// If you don't clean it,:The timer continues to run even after the component is unloaded,Error "Update on unmounted components"
(2) Cleaning Up Event Listeners
function WindowSize() {
const [size, setSize] = useState({ width: window.innerWidth, height: window.innerHeight })
useEffect(() => {
function handleResize() {
setSize({ width: window.innerWidth, height: window.innerHeight })
}
window.addEventListener('resize', handleResize)
// ✅ Remove event listeners when the component is unmounted
return () => {
window.removeEventListener('resize', handleResize)
}
}, [])
return <p>Window:{size.width} × {size.height}</p>
}
(3) Request Cancellation (to Avoid Race Conditions)
function SearchResults({ query }) {
const [results, setResults] = useState([])
useEffect(() => {
let cancelled = false
fetch('/api/search?q=' + query)
.then(res => res.json())
.then(data => {
// ✅ If the component has been uninstalled or query Has changed,Do not update status
if (!cancelled) {
setResults(data)
}
})
// ✅ Cleanup:Uncheck
return () => {
cancelled = true
}
}, [query])
return <div>{results.map(r => <p key={r.id}>{r.name}</p>)}</div>
}
// When users quickly switch search terms:
// 1. Search"A" → useEffect Execute
// 2. Search"AB" → Clear Function Markers cancelled=true → useEffect Re-execute
// 3. Search"A" the response came back → cancelled=true → Do not update status(Avoid flashing)
▶ Example: Comprehensive Demonstration of Cleanup Functions
Output:
Subheading: "useEffect Life Cycle Demonstration". State: count, logs. side effects via useEffect. interval updates
// ============================================
// Example:useEffect Declaration Cycle Demonstration
// Features:Start the timer when mounted + Listening for Keyboard Events
// Clean up all resources upon uninstallation
// ============================================
function LifecycleDemo() {
const [count, setCount] = useState(0)
const [logs, setLogs] = useState([])
// Effect 1:Title Update
useEffect(() => {
document.title = `Count:${count}`
}, [count])
// Effect 2:Timer
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1)
}, 2000)
addLog('⏱️ The timer has started')
return () => {
clearInterval(id)
addLog('⏱️ The timer has been cleared.')
}
}, [])
// Effect 3:Keyboard Events
useEffect(() => {
function handleKey(e) {
if (e.key === ' ') {
setCount(c => c + 5)
}
}
window.addEventListener('keydown', handleKey)
return () => {
window.removeEventListener('keydown', handleKey)
addLog('⌨️ Keyboard listening has been removed')
}
}, [])
function addLog(msg) {
setLogs(prev => [...prev, `${new Date().toLocaleTimeString()} ${msg}`])
}
return (
<div>
<h2>useEffect Life Cycle Demonstration</h2>
<p>Count:{count} (+1 every 2s, Space +5)</p>
<div style={{ maxHeight: '200px', overflow: 'auto', backgroundColor: '#f5f5f5', padding: '10px', borderRadius: '4px', fontSize: '12px', fontFamily: 'monospace' }}>
{logs.map((log, i) => <div key={i}>{log}</div>)}
</div>
</div>
)
}
Output:
Count auto-increments +1 every 2s, Space +5. Log: "⏱️ Timer started". Unmount → "⏱️ Timer cleared", "⌨️ Keyboard listening removed"
5. Mapping useEffect to the Component Lifecycle
In the class component era, there were three lifecycle hooks; in the Hooks era, useEffect—one hook does the work of three:
graph LR
subgraph "The Lifecycle of Class Components"
A1[componentDidMount] --> A2[componentDidUpdate]
A2 --> A3[componentWillUnmount]
end
subgraph "Hooks 's useEffect"
B1["useEffect(fn, [])<br/>Execute when mounted"]
B2["useEffect(fn, [dep])<br/>Mount + run when deps change"]
B3["return cleanup<br/>Clean up before uninstalling or rerunning"]
end
| Class Component Lifecycle | Corresponding useEffect |
|---|---|
componentDidMount |
useEffect(() => { ... }, []) |
componentDidUpdate |
useEffect(() => { ... }) (countless sets) or useEffect(() => { ... }, [dep]) |
Cleanup functions in componentWillUnmount |
useEffect(() => { return () => { ... } }, []) |
▶ Example: Using a Single useEffect to Handle Multiple Lifecycle Phases
Output:
Subheading: "useEffect Life Cycle Demonstration". Displays: "useEffect Life Cycle Demonstration". State: count (setter: setCount), logs (setter: setLogs). useEffect manages side effects. Interval-based updates
// ============================================
// Example:Chat Room Component——One useEffect Covering the Entire Lifecycle
// ============================================
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([])
useEffect(() => {
// === componentDidMount + componentDidUpdate ===
console.log(`🟢 Connect to the chat room:${roomId}`)
const connection = createConnection(roomId)
connection.connect()
connection.on('message', (msg) => {
setMessages(prev => [...prev, msg])
})
// === componentWillUnmount + Clean Up Before Re-Execution ===
return () => {
console.log(`🔴 Leave the chat room:${roomId}`)
connection.disconnect()
}
}, [roomId]) // roomId When things change:Disconnect the old connection → Join a New Chat Room
return (
<div>
<h3>Chat Room:{roomId}</h3>
{messages.map((msg, i) => <p key={i}>{msg}</p>}
</div>
)
}
// Usage
<ChatRoom roomId="general" /> // Connect general Chat Room
<ChatRoom roomId="random" /> // roomId Changes → Disconnect general → Connect random
// When a component is unloaded → Disconnect the current connection(Execution of the cleanup function)
Output:
Mount: console "🟢 Connected to chat room: general". Change roomId: "🔴 Left: general" → "🟢 Connected: random". Unmount: disconnect.
6. useEffect vs. useLayoutEffect
React provides two "side-effect" hooks, which differ in terms of execution timing.
graph LR
A[Trigger Rendering] --> B[Computational Virtual DOM]
B --> C[DOM Update]
C --> D{useLayoutEffect?}
D -->|is | E[Execute in parallel<br/>Blocking Browser Rendering]
D -->|No| F[Browser rendering complete]
F --> G[useEffect Asynchronous Execution]
style E fill:#faad14,color:#000
style G fill:#52c41a,color:#fff
| Hook | Execution Timing | Blocks Rendering | Use Cases |
|---|---|---|---|
useEffect |
After browser rendering (asynchronous) | ❌ | 90% of scenarios: data requests, event listeners, logging |
useLayoutEffect |
After DOM updates, before browser rendering (synchronous) | ✅ | Measure DOM dimensions, read layout, synchronize animations |
import { useState, useEffect, useLayoutEffect } from 'react'
function MeasureExample() {
const [width, setWidth] = useState(0)
const ref = useRef(null)
// ❌ useEffect Read DOM Dimensions → Users may notice flickering
useEffect(() => {
setWidth(ref.current.offsetWidth) // Read After Drawing → May trigger additional rendering
}, [])
// ✅ useLayoutEffect → Read Before Drawing,Transparent to the user
useLayoutEffect(() => {
setWidth(ref.current.offsetWidth)
}, [])
return <div ref={ref}>Width:{width}px</div>
}
Rules: Start by writing
useEffect; only switch touseLayoutEffectif you encounter "flickering" or "layout jitter" issues.
7. Complete Example: Data Dashboard
// ============================================
// Complete Example:Data Dashboard(useEffect Integrated Applications)
// Features:Refresh data at regular intervals + Window Adaptation + Resource Cleanup
// ============================================
import { useState, useEffect } from 'react'
function Dashboard() {
const [data, setData] = useState(null)
const [isLive, setIsLive] = useState(true)
const [lastUpdate, setLastUpdate] = useState(null)
const [windowSize, setWindowSize] = useState({
width: window.innerWidth,
height: window.innerHeight
})
// Effect 1:First Load + Refresh at set intervals
useEffect(() => {
function fetchData() {
// Simulation API Request
const mockData = {
users: Math.floor(Math.random() * 1000) + 500,
orders: Math.floor(Math.random() * 200) + 100,
revenue: Math.floor(Math.random() * 50000) + 10000,
growth: (Math.random() * 20 - 5).toFixed(1)
}
setData(mockData)
setLastUpdate(new Date().toLocaleTimeString())
}
// Execute once immediately
fetchData()
// Refresh at set intervals
let interval
if (isLive) {
interval = setInterval(fetchData, 5000)
}
return () => {
clearInterval(interval)
}
}, [isLive]) // isLive Reset the timer when a change occurs
// Effect 2:Window Size Monitoring
useEffect(() => {
function handleResize() {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight
})
}
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [])
// Effect 3:Page Title
useEffect(() => {
document.title = `Dashboard - ${data ? `${data.users} User` : 'Loading......'}`
}, [data])
// Effect 4:Leave Page Prompt
useEffect(() => {
function handleBeforeUnload(e) {
if (isLive) {
e.preventDefault()
e.returnValue = ''
}
}
window.addEventListener('beforeunload', handleBeforeUnload)
return () => window.removeEventListener('beforeunload', handleBeforeUnload)
}, [isLive])
return (
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
<h2>📊 Real-Time Dashboard</h2>
<div>
<span style={{ color: '#666', fontSize: '14px', marginRight: '12px' }}>
Window:{windowSize.width}×{windowSize.height}
</span>
<label>
<input type="checkbox" checked={isLive} onChange={e => setIsLive(e.target.checked)} />
Refreshes in real time
</label>
</div>
</div>
{!data ? (
<p style={{ textAlign: 'center', padding: '40px', color: '#999' }}>
⏳ Loading......
</p>
) : (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '16px' }}>
<StatCard label="Total Users" value={data.users.toLocaleString()} color="#1890ff" />
<StatCard label="Today's Orders" value={data.orders.toLocaleString()} color="#52c41a" />
<StatCard label="Monthly Income" value={`$${data.revenue.toLocaleString()}`} color="#ff4d4f" />
<StatCard label="Growth Rate" value={`${data.growth}%`} color={parseFloat(data.growth) >= 0 ? '#52c41a' : '#ff4d4f'} />
</div>
)}
<p style={{ textAlign: 'right', color: '#999', fontSize: '12px', marginTop: '12px' }}>
Last Updated:{lastUpdate} {isLive ? '(Auto-refresh every 5s)' : '(Suspended)'}
</p>
</div>
)
}
function StatCard({ label, value, color }) {
return (
<div style={{
border: '1px solid #f0f0f0',
borderRadius: '8px',
padding: '20px',
textAlign: 'center',
boxShadow: '0 2px 4px rgba(0,0,0,0.05)'
}}>
<p style={{ color: '#666', margin: '0 0 8px 0' }}>{label}</p>
<p style={{ fontSize: '28px', fontWeight: 'bold', color, margin: 0 }}>{value}</p>
</div>
)
}
Expected Output: A data dashboard containing 4 statistics cards, a real-time refresh toggle, a window size indicator, and the last update time.
▶ Example 3: Using useEffect to Implement Real-Time Data Synchronization
Output:
🟢 Connect to the chat room:...
🔴 Leave the chat room:...
function LiveSearch({ apiBase }) {
const [query, setQuery] = useState('')
const [results, setResults] = useState([])
const [loading, setLoading] = useState(false)
useEffect(() => {
if (!query.trim()) { setResults([]); return }
setLoading(true)
const controller = new AbortController()
fetch(`${apiBase}/search?q=${encodeURIComponent(query)}`, { signal: controller.signal })
.then(res => res.json())
.then(data => { setResults(data); setLoading(false) })
.catch(err => { if (err.name !== 'AbortError') setLoading(false) })
return () => controller.abort()
}, [query, apiBase])
return (
<div style={{ maxWidth: 400, margin: '0 auto' }}>
<input value={query} onChange={e => setQuery(e.target.value)}
placeholder="Search users..." style={{ width: '100%', padding: 8, borderRadius: 4, marginBottom: 8 }} />
{loading && <p>Searching...</p>}
<ul style={{ listStyle: 'none', padding: 0 }}>
{results.map(r => <li key={r.id} style={{ padding: 4, borderBottom: '1px solid #f0f0f0' }}>{r.name}</li>)}
</ul>
</div>
)
}
Output:
useEffect manages side effects
▶ Example 4: useEffect to monitor changes in window size
Output:
Displays: "Searching...". State: query (setter: setQuery), results (setter: setResults), loading (setter: setLoading). Input: Search users.... List: {r.name}. useEffect manages side effects. Async data fetching/loading states
function useWindowSize() {
const [size, setSize] = useState({ width: window.innerWidth, height: window.innerHeight })
useEffect(() => {
function handleResize() {
setSize({ width: window.innerWidth, height: window.innerHeight })
}
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [])
return size
}
function ResponsiveLayout() {
const { width } = useWindowSize()
const layout = width < 640 ? 'mobile' : width < 1024 ? 'tablet' : 'desktop'
return (
<div style={{ padding: 16, maxWidth: 600, margin: '0 auto' }}>
<h3>Viewport: {width}px ({layout})</h3>
{layout === 'mobile' && <p>📱 Single column layout</p>}
{layout === 'tablet' && <p>📟 Two column layout</p>}
{layout === 'desktop' && <p>💻 Three column layout with sidebar</p>}
</div>
)
}
Output:
useEffect manages side effects
▶ Example 5: useEffect + localStorage for Persisting State
Output:
Displays: "📱 Single column layout". State: size (setter: setSize). useEffect manages side effects
function usePersistedState(key, initial) {
const [value, setValue] = useState(() => {
try {
const saved = localStorage.getItem(key)
return saved !== null ? JSON.parse(saved) : initial
} catch { return initial }
})
useEffect(() => {
try { localStorage.setItem(key, JSON.stringify(value)) }
catch (e) { console.error('Save failed:', e) }
}, [key, value])
return [value, setValue]
}
function TodoApp() {
const [todos, setTodos] = usePersistedState('todos', [])
const [input, setInput] = useState('')
function addTodo() {
if (!input.trim()) return
setTodos(prev => [...prev, { id: Date.now(), text: input.trim(), done: false }])
setInput('')
}
function toggleTodo(id) {
setTodos(prev => prev.map(t => t.id === id ? { ...t, done: !t.done } : t))
}
return (
<div style={{ maxWidth: 400, margin: '0 auto' }}>
<h3>Todos (persisted)</h3>
<div style={{ display: 'flex', gap: 8, marginBottom: 8 }}>
<input value={input} onChange={e => setInput(e.target.value)}
onKeyDown={e => e.key === 'Enter' && addTodo()}
placeholder="Add task..." style={{ flex: 1, padding: 8, borderRadius: 4 }} />
<button onClick={addTodo} style={{ padding: '8px 16px', cursor: 'pointer' }}>Add</button>
</div>
<ul style={{ listStyle: 'none', padding: 0 }}>
{todos.map(t => (
<li key={t.id} onClick={() => toggleTodo(t.id)}
style={{ padding: 4, cursor: 'pointer', textDecoration: t.done ? 'line-through' : 'none', color: t.done ? '#999' : '#333' }}>
{t.text}
</li>
))}
</ul>
</div>
)
}
Output:
Todo list saved to localStorage. Add tasks, click to toggle (strikethrough). Refresh page → tasks persist. Enter to add.
❓ FAQ
useEffect and writing code directly inside a component?useEffect runs only after rendering is complete, so it does not block the UI. Code written directly inside a component’s function body runs during rendering (which blocks rendering) and executes on every render. The key difference is that useEffect allows you to perform cleanup when the component unmounts.useEffect run before every re-execution?userId changes, the old request is cleaned up before a new one is initiated, ensuring that the new data overwrites the old data. If the new request were to overwrite the old one directly, a bug could occur where the "old request completes first → data reverts."useEffect hooks?useEffect hooks that handle "initialization" (such as data loading) first, and those that handle "responding to changes" (such as syncing state to localStorage) last. During a single render, all useEffect hooks are executed in sequence.useEffect and useLayoutEffect?useEffect runs asynchronously after the DOM is updated (after the browser renders), so users won’t see any flickering; useLayoutEffect runs synchronously after the DOM is updated (before the browser renders), allowing you to modify the DOM and then have the browser render it. Use useEffect in 99% of cases. Use useLayoutEffect only when you "need to calculate based on the DOM layout and then synchronously modify the DOM" (such as adjusting an element’s position after measuring its dimensions).📖 Summary
useEffect(fn, deps)Execute side effects after rendering is complete, without blocking the UI- Empty dependency
[]= Executed only once during mounting; Non-empty dependency = Executed during mounting and when dependencies change - Cleanup functions
return () => {}are key to preventing memory leaks—timers, event listeners, and subscriptions must all be cleaned up - Use
useEffectin 90% of cases; useuseLayoutEffectonly when measuring the DOM. - Multiple
useEffecthooks are executed in the order they are written
📝 Exercises
- Basic Problem (Difficulty ⭐): Create a
PageTitlecomponent. UseuseEffectto change the page title to "Welcome to My Website" when the component mounts, and restore it to the original title when it unmounts. - Advanced Exercise (Difficulty ⭐⭐): Create a
OnlineStatuscomponent that usesuseEffectto listen for events fromnavigator.onLineandonline/offline, and display the network status in real time. - Challenge (Difficulty: ⭐⭐⭐): Create a custom hook named
usePollingthat accepts the parametersurlandinterval, polls the interface once every interval milliseconds, and returns{ data, isLoading, error }. The hook must support pausing and resuming polling.