← Về danh sách bài họcBài 6/25

⚡ Bài 6: useEffect - Side Effects

⏱️ Thời gian đọc: 18 phút | 📚 Độ khó: Trung bình

🎯 Sau bài học này, bạn sẽ:

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