← Về danh sách bài họcBài 6/25
⚡ Bài 6: useEffect - Side Effects
🎯 Sau bài học này, bạn sẽ:
- Hiểu Side Effects và tại sao cần useEffect
- Sử dụng dependency array đúng cách
- Viết cleanup function để tránh memory leak
- Fetch data từ API trong useEffect
- Biết các lỗi phổ biến và cách khắc phục
1. Side Effect Là Gì?
Side effect là bất kỳ thao tác nào bên ngoài phạm vi render của component: gọi API, DOM manipulation, subscriptions, timers, localStorage...
import { useState, useEffect } from 'react';
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
// Side effect: tạo interval
const id = setInterval(() => {
setSeconds(prev => prev + 1);
}, 1000);
// Cleanup: hủy interval khi unmount
return () => clearInterval(id);
}, []); // [] = chỉ chạy 1 lần khi mount
return <p>⏱️ {seconds} giây</p>;
}
2. Dependency Array
// 1️⃣ Không có dependency: chạy SAU MỖI lần render
useEffect(() => {
console.log('Chạy mỗi render');
});
// 2️⃣ Mảng rỗng []: chạy 1 lần khi MOUNT
useEffect(() => {
console.log('Chạy 1 lần');
return () => console.log('Cleanup khi unmount');
}, []);
// 3️⃣ Có dependencies: chạy khi dependencies thay đổi
useEffect(() => {
console.log(`userId changed: ${userId}`);
fetchUser(userId);
}, [userId]);
// 4️⃣ Nhiều dependencies
useEffect(() => {
fetchData(page, filter);
}, [page, filter]); // chạy khi page HOẶC filter thay đổi
⚠️ Lỗi phổ biến: Quên thêm dependency vào mảng → dùng giá trị cũ (stale closure). ESLint plugin
react-hooks/exhaustive-deps sẽ cảnh báo bạn!
3. Fetch Data Từ API
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// Tạo AbortController để hủy request
const controller = new AbortController();
const fetchUser = async () => {
try {
setLoading(true);
setError(null);
const res = await fetch(
`https://api.example.com/users/${userId}`,
{ signal: controller.signal }
);
if (!res.ok) throw new Error('Fetch failed');
const data = await res.json();
setUser(data);
} catch (err) {
if (err.name !== 'AbortError') {
setError(err.message);
}
} finally {
setLoading(false);
}
};
fetchUser();
// Cleanup: hủy request nếu userId thay đổi hoặc unmount
return () => controller.abort();
}, [userId]);
if (loading) return <p>Đang tải...</p>;
if (error) return <p>Lỗi: {error}</p>;
return <h2>{user?.name}</h2>;
}
💡 Best Practice: Luôn dùng
AbortController khi fetch data trong useEffect. Điều này ngăn race conditions khi user chuyển trang nhanh hoặc dependency thay đổi liên tục.
4. Cleanup Function
function WindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight
});
};
// Subscribe
window.addEventListener('resize', handleResize);
// Cleanup: unsubscribe khi unmount
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
return <p>{size.width} x {size.height}</p>;
}
📝 Tóm Tắt
useEffectxử lý side effects: API, timers, DOM, subscriptions- Dependency array:
[]mount only,[a, b]khi a/b đổi - Cleanup function: return hàm dọn dẹp để tránh memory leak
- Dùng
AbortControllerkhi fetch data - Luôn khai báo đầy đủ dependencies