[React Basics] useEffect Hook: Hiểu sâu về dependencies và vòng đời component

Trong thế giới React, chúng ta thường tập trung vào việc render giao diện người dùng (UI) dựa trên propsstate. Nhưng ứng dụng thực tế không chỉ có vậy. Chúng ta cần gọi API để lấy dữ liệu, thiết lập các bộ lắng nghe sự kiện, hay thay đổi trực tiếp DOM. Những hành động này được gọi là "side effects", vì chúng là những tác vụ "ngoài luồng", không liên quan trực tiếp đến việc "vẽ" UI.

Và để quản lý những tác vụ này một cách ngăn nắp và hiệu quả trong functional components, React đã mang đến cho chúng ta một "trợ thủ" vô cùng đắc lực: Hook useEffect.

useEffect Hook: Hiểu sâu về dependencies và vòng đời component

Bài viết này sẽ giúp bạn làm chủ hoàn toàn useEffect, từ những khái niệm cơ bản nhất đến các kỹ thuật nâng cao và những cạm bẫy cần tránh.

1. useEffect là gì? Tại sao nó lại quan trọng? 🧠

Hãy tưởng tượng component của bạn là một diễn viên trên sân khấu. Nhiệm vụ chính của anh ta là diễn (render UI) dựa trên kịch bản (propsstate). Tuy nhiên, giữa các cảnh diễn, anh ta cần làm những việc trong hậu trường như thay trang phục, chuẩn bị đạo cụ, hay nhận chỉ đạo từ đạo diễn.

useEffect chính là nơi để thực hiện những công việc "hậu trường" đó.

Nói theo cách kỹ thuật, useEffect là một Hook cho phép bạn thực hiện các side effects trong functional components. Nó là sự kết hợp sức mạnh của các phương thức lifecycle trong Class Component như componentDidMount, componentDidUpdate, và componentWillUnmount.

Bạn cần useEffect khi muốn component của mình làm một điều gì đó:

  • Sau khi nó vừa được render lần đầu tiên (ví dụ: gọi API để lấy danh sách bài viết).
  • Sau mỗi lần nó được render lại (ví dụ: cập nhật tiêu đề trang web dựa trên một state).
  • Ngay trước khi nó bị gỡ khỏi cây DOM (ví dụ: dọn dẹp một subscription hoặc một bộ lắng nghe sự kiện).

2. Cú pháp và Cách hoạt động

Cú pháp cơ bản của useEffect trông như thế này:

import React, { useState, useEffect } from 'react'

function MyComponent() {
  // ...
  useEffect(() => {
    // Hàm callback chứa logic của side effect
    console.log('Component đã được render!')

    // (Tùy chọn) Hàm dọn dẹp (cleanup function)
    return () => {
      console.log('Dọn dẹp trước lần render tiếp theo hoặc trước khi unmount.')
    }
  }, [dependencies]) // Mảng phụ thuộc (dependency array)
  // ...
}

Hãy cùng "mổ xẻ" 3 phần quan trọng nhất:

a. Hàm Callback (The Effect)

Đây là nơi bạn viết code cho side effect của mình. Đó có thể là một lệnh fetch dữ liệu, một document.title = ..., hay setInterval. Hàm này sẽ được React thực thi sau khi quá trình render hoàn tất và trình duyệt đã "vẽ" xong UI, đảm bảo nó không làm chặn quá trình hiển thị.

b. Mảng phụ thuộc (Dependency Array) - Trái tim của useEffect ❤️

Đây là phần quan trọng nhất và thường gây nhầm lẫn nhất. Mảng này quyết định khi nào effect của bạn sẽ được chạy lại. Có 3 trường hợp chính:

Trường hợp 1: Không cung cấp mảng phụ thuộc

useEffect(() => {
  // Chạy sau MỌI LẦN render
  console.log('Re-rendered!')
})

Effect sẽ chạy sau lần render đầu tiên và sau mỗi lần component render lại, bất kể lý do là gì. Trường hợp này ít được sử dụng vì có thể gây ra các vấn đề về hiệu năng và các vòng lặp vô hạn nếu bạn cập nhật state bên trong effect.

Trường hợp 2: Mảng rỗng []

useEffect(() => {
  // Chỉ chạy MỘT LẦN duy nhất sau lần render đầu tiên
  fetchData()
}, [])

Đây là cách hoàn hảo để thực hiện các tác vụ chỉ cần làm một lần khi component được "mount" (gắn vào DOM), tương đương với componentDidMount. Ví dụ điển hình là gọi API để lấy dữ liệu ban đầu.

Trường hợp 3: Mảng có giá trị [prop, state]

const [userId, setUserId] = useState(1)

useEffect(() => {
  // Chạy lần đầu, và chạy lại MỖI KHI userId thay đổi
  fetchUserProfile(userId)
}, [userId])

Effect sẽ chạy sau lần render đầu tiên, và sau đó, React sẽ theo dõi các giá trị trong mảng (userId trong ví dụ này). Nếu bất kỳ giá trị nào trong mảng thay đổi giữa các lần render, effect sẽ được kích hoạt lại. Đây là kịch bản mạnh mẽ và phổ biến nhất, tương đương với componentDidUpdate.

c. Hàm dọn dẹp (Cleanup Function) 🧹

Một số side effects cần được "dọn dẹp" khi không còn cần thiết nữa để tránh rò rỉ bộ nhớ (memory leaks). Ví dụ:

  • Nếu bạn thiết lập một setInterval, bạn cần clearInterval khi component bị hủy.
  • Nếu bạn thêm một event listener vào window, bạn cần gỡ nó ra.

Để làm điều này, bạn chỉ cần return một hàm từ bên trong effect của bạn. React sẽ tự động gọi hàm return này:

  1. Trước khi component unmount (bị gỡ khỏi DOM).
  2. Trước khi effect chạy lại ở lần render tiếp theo (để dọn dẹp effect của lần render trước đó).

Ví dụ kinh điển:

useEffect(() => {
  const handleResize = () => console.log('Window resized!')

  // Thêm event listener
  window.addEventListener('resize', handleResize)
  console.log('Event listener đã được thêm!')

  // Hàm dọn dẹp
  return () => {
    // Gỡ event listener
    window.removeEventListener('resize', handleResize)
    console.log('Event listener đã được gỡ bỏ!')
  }
}, []) // Chỉ chạy 1 lần

3. "Cạm bẫy" thường gặp và "Bí kíp" xử lý ⚠️

Nắm vững useEffect cũng có nghĩa là biết cách né tránh các lỗi phổ biến.

Cạm bẫy 1: Vòng lặp vô hạn (Infinite Loop)

Đây là lỗi kinh điển của người mới bắt đầu.

function MyComponent() {
  const [count, setCount] = useState(0)

  // ⚠️ LỖI: Vòng lặp vô hạn!
  useEffect(() => {
    // 1. Effect này chạy sau mỗi lần render.
    // 2. Nó cập nhật state 'count'.
    // 3. Cập nhật state gây ra một lần render mới.
    // 4. Component render lại, và effect lại chạy... -> Vô hạn!
    setCount((prevCount) => prevCount + 1)
  })
  // ...
}

Cách khắc phục: Luôn cung cấp mảng phụ thuộc! Hãy tự hỏi: "Effect này nên chạy lại khi nào?". Nếu chỉ cần chạy 1 lần, dùng []. Nếu cần chạy khi một giá trị thay đổi, hãy đưa giá trị đó vào mảng.

Cạm bẫy 2: "Stale" State/Props (Dữ liệu cũ)

Điều này xảy ra khi bạn quên không đưa một giá trị mà effect có sử dụng vào mảng phụ thuộc.

function Counter() {
  const [count, setCount] = useState(0)

  useEffect(() => {
    const intervalId = setInterval(() => {
      // ⚠️ LỖI: 'count' ở đây luôn là 0!
      // Hàm này đã "bắt" (closure) giá trị của 'count' tại thời điểm nó được tạo ra (lần render đầu tiên).
      // Nó sẽ không bao giờ thấy được giá trị 'count' đã được cập nhật.
      console.log(`Count is: ${count}`)
    }, 1000)

    return () => clearInterval(intervalId)
  }, []) // Mảng rỗng, nên effect này không bao giờ chạy lại

  // ...
}

Cách khắc phục:

  1. Thêm count vào mảng phụ thuộc: [count]. Điều này sẽ dọn dẹp interval cũ và tạo một interval mới mỗi khi count thay đổi.
  2. Sử dụng functional update: setCount(c => c + 1). Bằng cách này, bạn không cần truy cập trực tiếp vào count, do đó không cần đưa nó vào mảng phụ thuộc.

Bí kíp: Luôn bật quy tắc ESLint react-hooks/exhaustive-deps. Nó sẽ cảnh báo bạn khi bạn quên một dependency nào đó.

Bí kíp 1: Tách biệt các Effect theo chức năng

Đừng nhồi nhét tất cả logic vào một useEffect duy nhất. Nếu bạn có các tác vụ không liên quan, hãy tách chúng ra.

// 👎 KHÔNG NÊN
useEffect(() => {
  fetchData(id)
  document.title = `User ${id}`
}, [id])

// 👍 NÊN
useEffect(() => {
  fetchData(id)
}, [id])

useEffect(() => {
  document.title = `User ${id}`
}, [id])

Việc này giúp code dễ đọc, dễ bảo trì và tuân thủ nguyên tắc Single Responsibility.

Bí kíp 2: Trừu tượng hóa logic bằng Custom Hooks

Nếu bạn thấy mình lặp đi lặp lại một đoạn logic useEffect ở nhiều component (ví dụ: logic gọi API, logic lắng nghe sự kiện), hãy đóng gói nó vào một Custom Hook.

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

// Sử dụng trong component
function UserProfile({ userId }) {
  const user = useFetch(`/api/users/${userId}`)
  // ...
}

Kết luận: useEffect không phải là một khái niệm phức tạp

useEffect không phải là một khái niệm phức tạp, nhưng nó đòi hỏi sự tỉ mỉ. Để thực sự làm chủ nó, hãy luôn ghi nhớ 3 điều cốt lõi:

  1. Hàm Effect: Code của bạn sẽ làm gì?
  2. Mảng phụ thuộc: Code của bạn sẽ chạy lại khi nào?
  3. Hàm dọn dẹp: Code của bạn cần dọn dẹp những gì sau khi hoàn thành?

Khi bạn đã thấm nhuần tư duy này, useEffect sẽ trở thành một công cụ cực kỳ mạnh mẽ, giúp bạn xây dựng các ứng dụng React phức tạp, hiệu quả và không bị rò rỉ bộ nhớ.

Chúc bạn thành công trên con đường chinh phục React!

Bài viết liên quan

[React Basics] useState Hook: Cách quản lý State hiệu quả trong React

Hướng dẫn cách sử dụng useState Hook để quản lý state trong các function component của React. Tìm hiểu các ví dụ thực tế, từ cơ bản đến nâng cao, giúp bạn làm chủ React Hook quan trọng này.

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

Bạn đang tìm hiểu về React Hook? Bài viết này sẽ giải thích React Hook là gì, các loại Hook phổ biến và hướng dẫn từng bước cách áp dụng chúng vào dự án thực tế.

[React Basics] React Context: Khái niệm & Cách sử dụng hiệu quả nhất

React Context là gì? Học cách sử dụng React Context API để quản lý state toàn cục dễ dàng và hiệu quả, loại bỏ Prop Drilling và tối ưu hóa ứng dụng React.

[React Basics] React là gì? Tại sao nên học React ngay hôm nay?

Tìm hiểu React là gì, tại sao nó là một trong những thư viện JavaScript phổ biến nhất thế giới. Nắm bắt được cách React xây dựng giao diện người dùng nhanh chóng và hiệu quả.