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

import { useState, useEffect } from 'react'

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

SyntaxExecution TimingUse Case
useEffect(fn)Runs after every renderRarely used; prone to infinite loops
useEffect(fn, [])Runs only once after the initial renderData loading, initializing third-party libraries
useEffect(fn, [dep])Runs after initial render + when dep changesListen 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

// File path: public/posts.json
[
  {
    "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

// File path: src/pages/HomePage.jsx
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

useEffect(() => {
  // 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 EffectNeeds cleanup?Cleanup Method
fetch requestYesUse cancelled flag or AbortController
Event listenerYesremoveEventListener
TimerYesclearInterval / clearTimeout
Modify DOM then restoreYesRestore original state in cleanup
localStorage writesnoNo cleanup needed

Four States for Handling Async Requests

Any data request should cover the following four UI states:

StateConditionUI
LoadingisLoading === trueLoading animation, skeleton screen
SuccessData has been returnedRender content normally
Failureerror !== nullError message + retry button
Empty dataData is an empty arrayEmpty state message

Comparison with Vue3 Lifecycle

Vue3ReactDescription
onMounteduseEffect(fn, [])Runs after the initial render
watch(keyword, fn)useEffect(fn, [keyword])Responds to specific data changes
onBeforeUnmountuseEffect 的 cleanupCleanup before component destruction
No direct counterpartuseEffect(() => { 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