Side Effects and Data Fetching — useEffect
In this chapter, you will learn React's most important side effect Hook — useEffect, and use fetch to load blog data from a JSON file at the right time.
What is a Side Effect?
In React,Side Effectrefers to operations that are unrelated to rendering:
- Requesting data from the server
- Manipulating localStorage
- Setting timers with setInterval
- Manually modifying the DOM
- Subscribing to external events
Side effects cannot be executed directly in the component function body (which would cause them to run on every render); they must be placed inuseEffect.
Basic Usage of useEffect
Example
function HomePage() {
const [message, setMessage] = useState('Loading...')
// useEffect(fn, deps): executes the side effect after the component renders
useEffect(() => {
console.log('The component has been mounted to the DOM; side effects can be executed now.')
setMessage('Data loading complete!')
}, []) // Empty dependency array = runs only once after the initial render
return <p>{message}</p>
}
Three Ways to Write the Dependency Array
| Syntax | Execution Timing | Use Case |
|---|---|---|
useEffect(fn) | Runs after every render | Rarely used; prone to infinite loops |
useEffect(fn, []) | Runs only once after the initial render | Data loading, initializing third-party libraries |
useEffect(fn, [dep]) | Runs after initial render + when dep changes | Listen to specific data changes and execute corresponding operations |
Difference between omitting the dependency array vs an empty array:
useEffect(fn)Runs after every render;useEffect(fn, [])It only runs once after the initial render. The former can easily cause infinite loops (setState in effect → triggers render → effect runs again → ...), so it is almost never used.
Loading JSON Data with fetch
Just like Vue3, we put the article data into thepublic/posts.jsonfile.
Step 1: Create the Data File
Example
[
{
"id": 1,
"title": "The Complete Guide to Getting Started with React",
"summary": "Learn React Hooks from scratch, covering core concepts such as useState and useEffect.",
"content": "<h2>Why learn React?</h2><p>React is one of the most popular front-end frameworks...</p>",
"category": "React",
"date": "2024-05-10"
},
{
"id": 2,
"title": "JavaScript Asynchronous Programming Explained",
"summary": "Understand Promise, async/await, event loop, and microtask queue in one article.",
"content": "<h2>What is asynchronous?</h2><p>JS is single-threaded...</p>",
"category": "JavaScript",
"date": "2024-05-08"
},
{
"id": 3,
"title": "CSS Grid Layout in Practice",
"summary": "Easily implement complex responsive layouts with CSS Grid.",
"content": "<h2>Getting Started with Grid</h2><p>Grid is a two-dimensional layout system...</p>",
"category": "CSS",
"date": "2024-05-05"
}
]
Step 2: Load Data in useEffect
Example
import { useState, useEffect, useMemo } from 'react'
import BlogCard from '../components/BlogCard'
import CategoryFilter from '../components/CategoryFilter'
function HomePage() {
const [articles, setArticles] = useState([]) // Article data
const [isLoading, setIsLoading] = useState(true) // Loading state
const [error, setError] = useState(null) // Error message
const [activeCategory, setActiveCategory] = useState('All')
// useEffect loads data: empty dependency array = runs only once after the initial render
useEffect(() => {
let cancelled = false // Flag to prevent setState after the component unmounts
async function fetchPosts() {
setIsLoading(true)
setError(null)
try {
const res = await fetch('/posts.json')
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const data = await res.json()
if (!cancelled) {
setArticles(data)
}
} catch (err) {
if (!cancelled) {
setError(err.message)
}
} finally {
if (!cancelled) {
setIsLoading(false)
}
}
}
fetchPosts()
// Cleanup function: set cancelled to true when the component unmounts
return () => { cancelled = true }
}, [])
const categories = useMemo(() => {
return ['All', ...new Set(articles.map(a => a.category))]
}, [articles])
const filteredArticles = useMemo(() => {
if (activeCategory === 'All') return articles
return articles.filter(a => a.category === activeCategory)
}, [articles, activeCategory])
return (
<div>
<h2 className="section-title">Latest Articles</h2>
{/* Loading */}
{isLoading && <p className="status-msg">Loading, please wait...</p>}
{/* Loading Error */}
{error && (
<div className="status-msg error">
<p>Loading failed:{error}</p>
<button onClick={() => window.location.reload()}>Retry</button>
</div>
)}
{/* Data Loading Complete */}
{!isLoading && !error && (
<>
<CategoryFilter
categories={categories}
activeCategory={activeCategory}
onCategoryChange={setActiveCategory}
/>
<p className="result-info">common{filteredArticles.length}article</p>
{filteredArticles.length === 0 ? (
<p className="empty-tip">No articles in this category yet</p>
) : (
<div className="article-grid">
{filteredArticles.map(article => (
<BlogCard key={article.id} {...article} />
))}
</div>
)}
</>
)}
</div>
)
}
export default HomePage
cancelledThe flag is a common pattern in React. When fetch is still in progress and the user leaves the page (component unmounts), the subsequent setState will report an error warning. Setting it in the cleanup functioncancelled = truecan skip setState after unmount.
Cleanup Function
The callback function of useEffect can return a function, known as thecleanup function。
It runs at two moments:When the component unmountsandBefore the next effect executes。
Example
// Side effect: subscribe to events
const handleScroll = () => console.log('Scrolled')
window.addEventListener('scroll', handleScroll)
// Return the cleanup function
return () => {
// Automatically remove the event listener when the component unmounts to prevent memory leaks
window.removeEventListener('scroll', handleScroll)
}
}, [])
useEffect(() => {
// Side effect: set a timer
const timer = setInterval(() => {
console.log('Executes every second')
}, 1000)
return () => clearInterval(timer) // Clear the timer
}, [])
| Side Effect | Needs cleanup? | Cleanup Method |
|---|---|---|
| fetch request | Yes | Use cancelled flag or AbortController |
| Event listener | Yes | removeEventListener |
| Timer | Yes | clearInterval / clearTimeout |
| Modify DOM then restore | Yes | Restore original state in cleanup |
| localStorage writes | no | No cleanup needed |
Four States for Handling Async Requests
Any data request should cover the following four UI states:
| State | Condition | UI |
|---|---|---|
| Loading | isLoading === true | Loading animation, skeleton screen |
| Success | Data has been returned | Render content normally |
| Failure | error !== null | Error message + retry button |
| Empty data | Data is an empty array | Empty state message |
Comparison with Vue3 Lifecycle
| Vue3 | React | Description |
|---|---|---|
onMounted | useEffect(fn, []) | Runs after the initial render |
watch(keyword, fn) | useEffect(fn, [keyword]) | Responds to specific data changes |
onBeforeUnmount | useEffect 的 cleanup | Cleanup before component destruction |
| No direct counterpart | useEffect(() => { fn; return cleanup }, [dep]) | When dep changes, run cleanup first, then fn |
Chapter Summary
In this chapter, you have mastered the core usage of useEffect: the three ways to write the dependency array, loading data asynchronously with fetch, handling the four UI states, and the role of the cleanup function.
Now the blog data is dynamically loaded from an external JSON file, no longer hardcoded in JS.
Other Extensions