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 props
và state
. 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
.
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 (props
và state
). 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ầnclearInterval
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:
- Trước khi component unmount (bị gỡ khỏi DOM).
- 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:
- 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 khicount
thay đổi. - 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àocount
, 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:
- Hàm Effect: Code của bạn sẽ làm gì?
- Mảng phụ thuộc: Code của bạn sẽ chạy lại khi nào?
- 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!