Side Effect trong React là gì? Giải thích chi tiết và Ví dụ thực tế

VnnTools

Khi bắt đầu hành trình với React, bạn sẽ tập trung vào việc xây dựng giao diện người dùng (UI) từ các component, state và props. Mọi thứ dường như rất "thuần khiết": component nhận đầu vào (props, state) và trả về đầu ra (JSX). Nhưng rồi một ngày, bạn tự hỏi: "Làm thế nào để lấy dữ liệu từ server? Làm sao để thay đổi tiêu đề trang web? Hay làm thế nào để thiết lập một bộ đếm thời gian?".

Side Effect trong React là gì? Giải thích chi tiết và Ví dụ thực tế

Chào mừng bạn đến với Side Effect (hay "tác dụng phụ") - một khái niệm cốt lõi nhưng thường gây bối rối cho người mới. Đừng lo lắng! Bài viết này sẽ giải mã tất cả mọi thứ về side effect một cách trực quan và dễ hiểu nhất.

🤔 Side Effect là gì? Một cách hiểu đơn giản nhất

Hãy tưởng tượng component React của bạn là một người đầu bếp chuyên nghiệp 👨‍🍳. Nhiệm vụ chính của anh ta là nhận nguyên liệu (props, state) và nấu ra một món ăn hoàn hảo (render ra UI). Quá trình này được gọi là "pure function" - một quy trình khép kín, có thể dự đoán được.

Side effect chính là tất cả những hành động mà người đầu bếp phải làm bên ngoài căn bếp của mình.

Ví dụ:

  • Đi ra chợ mua nguyên liệu: Tương đương với việc gọi API để lấy dữ liệu từ server.
  • Hẹn giờ cho lò nướng: Tương đương với việc sử dụng setTimeout hoặc setInterval.
  • Ghi lại công thức vào sổ tay: Tương đương với việc ghi log hoặc lưu dữ liệu vào localStorage.
  • Thay đổi biển hiệu "Món ăn của ngày": Tương đương với việc thao tác trực tiếp lên DOM (ví dụ: thay đổi document.title).

Tóm lại, side effect trong React là bất kỳ hành động nào mà component của bạn thực hiện để tương tác với thế giới bên ngoài luồng render thông thường của nó. Thế giới bên ngoài đó có thể là một API, Local Storage, DOM của trình duyệt, hoặc bất kỳ hệ thống nào khác không do React kiểm soát trực tiếp.

💥 Tại sao phải "quản lý" Side Effect?

Tại sao chúng ta không thể đặt thẳng một lệnh fetch() vào trong thân component?

// ⛔️ ĐỪNG LÀM THẾ NÀY!
function MyComponent() {
  // Sai lầm! Lệnh fetch này sẽ chạy mỗi khi component render lại
  fetch('https://api.example.com/data')
    .then((res) => res.json())
    .then((data) => console.log(data))

  return <div>My Data</div>
}

Vấn đề là React có thể render lại một component nhiều lần vì nhiều lý do (state thay đổi, props thay đổi...). Nếu bạn đặt side effect trực tiếp như trên, nó sẽ chạy lại mỗi lần render. Điều này dẫn đến những hậu quả nghiêm trọng:

  • Vòng lặp vô tận (Infinite Loop): Gọi API, sau đó cập nhật state, việc cập nhật state lại gây ra render, render lại gọi API... và cứ thế lặp lại.
  • Hiệu năng kém: Gửi hàng trăm yêu cầu mạng không cần thiết.
  • Hành vi khó đoán: Bạn không thể kiểm soát được khi nào side effect sẽ xảy ra.

Vì vậy, React cần một "khu vực an toàn", một nơi đặc biệt để chúng ta có thể thực thi và quản lý các side effect này một cách có kiểm soát. Và người hùng đó chính là...

🦸‍♂️ useEffect - Vũ khí tối thượng để xử lý Side Effect

useEffect là một Hook được React cung cấp, cho phép bạn thực hiện các side effect từ bên trong function component. Nó giống như bạn nói với React: "Này React, sau khi anh render xong UI, hãy chạy giúp tôi đoạn mã này nhé!".

Cú pháp cơ bản

useEffect nhận vào hai đối số:

  1. Một hàm (function) chứa mã side effect của bạn.
  2. Một mảng phụ thuộc (dependency array) (tùy chọn) để kiểm soát khi nào effect được chạy lại.
import { useEffect } from 'react'

useEffect(() => {
  // Mã side effect của bạn sẽ nằm ở đây
  console.log('Component đã được render hoặc cập nhật!')
}, [dependencies]) // <-- Mảng phụ thuộc

useEffect là một chiếc chìa khóa vạn năng, và sức mạnh của nó nằm ở mảng phụ thuộc.

Ba kịch bản của mảng phụ thuộc

  1. Không có mảng phụ thuộc: Effect chạy sau mỗi lần render.

    useEffect(() => {
      // ⚠️ Cẩn thận: Chạy sau mỗi lần component render lại.
      // Rất dễ gây ra vòng lặp vô tận.
    })
    
  2. Mảng rỗng []: Effect chỉ chạy một lần duy nhất, ngay sau lần render đầu tiên.

    useEffect(() => {
      // Tuyệt vời để gọi API một lần hoặc thiết lập ban đầu.
      fetchData()
    }, []) // <-- Mảng rỗng
    
  3. Mảng có giá trị [prop, state]: Effect sẽ chạy lần đầu, và sau đó chỉ chạy lại khi một trong các giá trị trong mảng thay đổi.

    const [userId, setUserId] = useState(1)
    
    useEffect(() => {
      // Effect này sẽ chạy lại mỗi khi userId thay đổi.
      fetchUser(userId)
    }, [userId]) // <-- Phụ thuộc vào userId
    

    Đây là kịch bản mạnh mẽ và được sử dụng nhiều nhất.

🧹 Chức năng Dọn dẹp (Cleanup Function)

Điều gì xảy ra nếu bạn thiết lập một setInterval hoặc một kết nối WebSocket? Nếu component bị gỡ khỏi cây DOM (unmount), những kết nối đó vẫn tồn tại và gây ra rò rỉ bộ nhớ (memory leak).

useEffect cho phép bạn trả về một hàm từ bên trong nó. Hàm này được gọi là "cleanup function" và sẽ được thực thi khi:

  • Component chuẩn bị unmount.
  • Trước khi effect chạy lại ở lần render tiếp theo.
useEffect(() => {
  const timerId = setInterval(() => {
    console.log('Tick!')
  }, 1000)

  // 👇 Đây là cleanup function
  return () => {
    console.log('Dọn dẹp timer...')
    clearInterval(timerId) // Hủy timer khi component unmount
  }
}, [])

🎯 Các trường hợp sử dụng useEffect phổ biến

Dưới đây là một số ví dụ thực tế:

1. Lấy dữ liệu từ API

function UserProfile({ userId }) {
  const [user, setUser] = useState(null)

  useEffect(() => {
    async function fetchUserData() {
      const response = await fetch(`https://api.example.com/users/${userId}`)
      const data = await response.json()
      setUser(data)
    }

    fetchUserData()
  }, [userId]) // Chạy lại khi userId thay đổi

  return <div>{user ? user.name : 'Loading...'}</div>
}

2. Thao tác DOM

function DocumentTitleChanger({ title }) {
  useEffect(() => {
    // Side effect: thay đổi tiêu đề tài liệu
    document.title = title
  }, [title]) // Cập nhật khi title thay đổi

  return <h1>Nội dung trang</h1>
}

3. Lắng nghe sự kiện

function WindowWidth() {
  const [width, setWidth] = useState(window.innerWidth)

  useEffect(() => {
    function handleResize() {
      setWidth(window.innerWidth)
    }

    window.addEventListener('resize', handleResize)

    // Cleanup: gỡ bỏ event listener khi component unmount
    return () => {
      window.removeEventListener('resize', handleResize)
    }
  }, []) // Chỉ cần thiết lập một lần

  return <div>Chiều rộng cửa sổ: {width}px</div>
}

💡 Xa hơn với Side Effect: Custom Hooks & React Query

Khi ứng dụng lớn dần, bạn sẽ thấy mình lặp lại logic useEffect ở nhiều nơi. Đây là lúc Custom Hooks tỏa sáng. Bạn có thể đóng gói logic side effect vào một hook tái sử dụng.

Ví dụ, tạo một useFetch hook:

function useFetch(url) {
  const [data, setData] = useState(null)
  useEffect(() => {
    fetch(url)
      .then((res) => res.json())
      .then((d) => setData(d))
  }, [url])
  return data
}

// Cách sử dụng trong component
function MyComponent() {
  const userData = useFetch('api/user')
  // ...
}

Đối với các side effect phức tạp liên quan đến dữ liệu server (caching, re-fetching, optimistic updates...), các thư viện chuyên dụng như TanStack Query (React Query) hoặc SWR là lựa chọn tuyệt vời, giúp bạn quản lý side effect một cách mạnh mẽ và ít code hơn.

Tóm lại: Những ý chính về Side Effect trong React

  • Side Effect là bất kỳ tương tác nào với "thế giới bên ngoài" component của bạn, như gọi API, thao tác DOM, hay timers.
  • Chúng ta phải quản lý side effect để tránh các lỗi như vòng lặp vô tận và hành vi không đoán trước.
  • useEffect là hook của React để xử lý side effect một cách có kiểm soát.
  • Mảng phụ thuộc là trái tim của useEffect, quyết định khi nào effect sẽ chạy lại.
  • Luôn nhớ dọn dẹp (cleanup) các side effect như subscriptions hay timers để tránh rò rỉ bộ nhớ.
  • Khi logic phức tạp, hãy cân nhắc tạo Custom Hooks hoặc sử dụng thư viện như React Query.

Hiểu và làm chủ useEffect là một bước ngoặt trong quá trình trở thành một lập trình viên React thành thạo. Hy vọng bài viết này đã giúp bạn "giải mã" thành công khái niệm quan trọng này!

Bài viết liên quan

Render trong React là gì? Bài giải thích chi tiết cho người mới

Tìm hiểu cơ chế Render trong React từ A đến Z. Bài viết giải thích rõ ràng khái niệm, cách React so sánh Virtual DOM và cập nhật UI hiệu quả.

State trong ReactJS: Khái niệm, cách sử dụng và ví dụ chi tiết

Tìm hiểu khái niệm, cách khai báo và sử dụng State để quản lý dữ liệu động trong ứng dụng React của bạn. Xem ngay hướng dẫn chi tiết kèm ví dụ.

JSX trong React là gì? Hướng dẫn chi tiết cho người mới bắt đầu

JSX là cú pháp mở rộng của JavaScript, cho phép viết code giống HTML trong React. Bài viết này sẽ giúp bạn hiểu rõ JSX là gì, tại sao cần dùng JSX và cách sử dụng hiệu quả.

Component React là gì? Hướng dẫn chi tiết cho người mới bắt đầu

Hiểu rõ Component React chính là chìa khóa để xây dựng giao diện ứng dụng web hiện đại. Đọc ngay để nắm vững các loại component phổ biến và cách tạo component đầu tiên của bạn.