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?".
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ặcsetInterval
. - 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ố:
- Một hàm (function) chứa mã side effect của bạn.
- 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
-
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. })
-
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
-
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!