Skip to content

📅 NGÀY 21: useRef - Fundamentals & Mutable Values

🎯 Mục tiêu học tập (5 phút)

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

  • [ ] Hiểu bản chất của useRef và khi nào cần dùng thay vì useState
  • [ ] Nắm vững cách useRef lưu trữ giá trị mutable không trigger re-render
  • [ ] Biết cách persist values across renders mà không gây side effects
  • [ ] Phân biệt rõ ràng use cases của useRef vs useState
  • [ ] Áp dụng useRef để giải quyết các vấn đề thực tế như tracking previous values, storing timer IDs

🤔 Kiểm tra đầu vào (5 phút)

Trước khi bắt đầu, hãy trả lời 3 câu hỏi này:

  1. Điều gì xảy ra khi bạn update state bằng setState?

    • Component re-render với giá trị mới
  2. useEffect cleanup function chạy khi nào?

    • Trước khi effect chạy lần tiếp theo hoặc khi component unmount
  3. Làm sao để store một giá trị persists across renders nhưng không muốn trigger re-render khi thay đổi?

    • Đây chính là vấn đề useRef giải quyết! 🎯

📖 PHẦN 1: GIỚI THIỆU KHÁI NIỆM (30 phút)

1.1 Vấn Đề Thực Tế

Hãy xem xét tình huống này:

jsx
// ❌ VẤN ĐỀ: Muốn store interval ID để clear sau này
function Timer() {
  const [count, setCount] = useState(0);
  let intervalId; // ⚠️ Sẽ bị reset mỗi lần render!

  const startTimer = () => {
    intervalId = setInterval(() => {
      setCount((c) => c + 1);
    }, 1000);
  };

  const stopTimer = () => {
    clearInterval(intervalId); // ⚠️ intervalId luôn là undefined!
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={startTimer}>Start</button>
      <button onClick={stopTimer}>Stop</button>
    </div>
  );
}

Vấn đề:

  • Biến intervalId được declare lại mỗi lần component re-render
  • stopTimer không thể access được intervalId từ startTimer
  • Timer không thể stop được! 😱

Bạn có thể nghĩ: "Dùng useState để lưu intervalId?"

jsx
// ❌ GIẢI PHÁP SAI: Dùng useState
function Timer() {
  const [count, setCount] = useState(0);
  const [intervalId, setIntervalId] = useState(null); // ⚠️ Overkill!

  const startTimer = () => {
    const id = setInterval(() => {
      setCount((c) => c + 1);
    }, 1000);
    setIntervalId(id); // ⚠️ Gây re-render không cần thiết!
  };

  const stopTimer = () => {
    clearInterval(intervalId);
    setIntervalId(null); // ⚠️ Lại re-render!
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={startTimer}>Start</button>
      <button onClick={stopTimer}>Stop</button>
    </div>
  );
}

Tại sao sai?

  • intervalId không phải UI data, không cần render
  • Mỗi lần set intervalId → re-render không cần thiết
  • Performance waste! 📉

1.2 Giải Pháp: useRef

jsx
// ✅ GIẢI PHÁP ĐÚNG: Dùng useRef
import { useState, useRef } from 'react';

function Timer() {
  const [count, setCount] = useState(0);
  const intervalRef = useRef(null); // 🎯 Perfect!

  const startTimer = () => {
    intervalRef.current = setInterval(() => {
      setCount((c) => c + 1);
    }, 1000);
  };

  const stopTimer = () => {
    clearInterval(intervalRef.current);
    intervalRef.current = null;
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={startTimer}>Start</button>
      <button onClick={stopTimer}>Stop</button>
    </div>
  );
}

Tại sao tốt hơn?

  • intervalRef.current persists across renders
  • Update intervalRef.current KHÔNG trigger re-render
  • Chỉ re-render khi count thay đổi (cần thiết cho UI)

1.3 Mental Model

Hãy tưởng tượng useRef như một chiếc hộp có ngăn kéo:

┌─────────────────────────────────┐
│   useRef Container              │
├─────────────────────────────────┤
│                                 │
│   ┌───────────────────┐         │
│   │  .current         │         │ ← Ngăn kéo này luôn ở đó
│   │  (mutable value)  │         │   giữa các lần render
│   └───────────────────┘         │
│                                 │
└─────────────────────────────────┘

Đặc điểm:
✅ Hộp (ref object) không bao giờ thay đổi
✅ Ngăn kéo (.current) có thể mở ra và thay đổi nội dung
✅ Thay đổi nội dung ngăn kéo KHÔNG làm React nhận biết

So sánh với useState:

useState                    useRef
──────────────────────────────────────────
const [value, setValue]     const ref = useRef(value)
setValue(newValue)          ref.current = newValue
→ Trigger re-render        → KHÔNG re-render
→ Async update             → Sync update
→ Cho UI data              → Cho non-UI data

1.4 Hiểu Lầm Phổ Biến

❌ Hiểu lầm 1: "useRef chỉ dùng để access DOM"

Thực tế: useRef có 2 use cases chính:

  1. Mutable values (hôm nay học)
  2. DOM references (ngày mai học)
jsx
// ✅ Use case 1: Mutable value
const countRef = useRef(0);
countRef.current += 1; // Không re-render

// ✅ Use case 2: DOM reference (sẽ học ngày mai)
const inputRef = useRef(null);
// <input ref={inputRef} />

❌ Hiểu lầm 2: "ref.current thay đổi thì component re-render"

jsx
// ❌ SAI: Nghĩ ref.current thay đổi → re-render
function Counter() {
  const countRef = useRef(0);

  const increment = () => {
    countRef.current += 1;
    console.log(countRef.current); // Giá trị tăng
    // ⚠️ Nhưng UI KHÔNG update!
  };

  return (
    <div>
      <p>Count: {countRef.current}</p> {/* Luôn hiển thị 0 */}
      <button onClick={increment}>+1</button>
    </div>
  );
}

❌ Hiểu lầm 3: "Có thể dùng useRef thay useState để tránh re-render"

jsx
// ❌ ANTI-PATTERN: Dùng ref cho UI data
function BadCounter() {
  const countRef = useRef(0);

  const increment = () => {
    countRef.current += 1;
    // ⚠️ UI không update vì không re-render!
  };

  return <p>Count: {countRef.current}</p>;
}

// ✅ ĐÚNG: Dùng useState cho UI data
function GoodCounter() {
  const [count, setCount] = useState(0);

  const increment = () => {
    setCount((c) => c + 1); // UI updates correctly
  };

  return <p>Count: {count}</p>;
}

💻 PHẦN 2: LIVE CODING (45 phút)

Demo 1: Pattern Cơ Bản - Tracking Render Count ⭐

jsx
/**
 * 🎯 Mục tiêu: Đếm số lần component render
 * 💡 Insight: useRef perfect cho tracking mà không gây re-render
 */

import { useState, useRef, useEffect } from 'react';

function RenderCounter() {
  const [count, setCount] = useState(0);
  const renderCount = useRef(0);

  // ⚠️ CHÚ Ý: Không dùng useEffect để increment!
  // Vì useEffect chạy AFTER render

  // ✅ Increment ngay trong render phase
  renderCount.current += 1;

  console.log(`Render #${renderCount.current}`);

  return (
    <div>
      <h2>Render Counter Demo</h2>
      <p>This component has rendered: {renderCount.current} times</p>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment Count</button>
    </div>
  );
}

// 🔍 Output Render #1 (initial)
// This component has rendered: 1 times | Count: 0

// 🔍 Khi click button:
// Click button → Render #2 | This component has rendered: 2 times | Count: 1
// Click button → Render #3 | This component has rendered: 3 times | Count: 2

Giải thích chi tiết:

jsx
// ❌ SAI: Dùng useState để count renders
function BadRenderCounter() {
  const [count, setCount] = useState(0);
  const [renderCount, setRenderCount] = useState(0);

  // ⚠️ Gây infinite loop!
  setRenderCount(renderCount + 1); // Trigger re-render → lại gọi setRenderCount → loop!

  return <p>Renders: {renderCount}</p>;
}

// ❌ SAI: Dùng biến thường
function BadRenderCounter2() {
  const [count, setCount] = useState(0);
  let renderCount = 0; // ⚠️ Reset về 0 mỗi render!

  renderCount += 1; // Luôn là 1

  return <p>Renders: {renderCount}</p>; // Luôn hiển thị 1
}

// ✅ ĐÚNG: Dùng useRef
function GoodRenderCounter() {
  const [count, setCount] = useState(0);
  const renderCount = useRef(0);

  renderCount.current += 1; // Persists, không trigger re-render

  return <p>Renders: {renderCount.current}</p>;
}

Demo 2: Kịch Bản Thực Tế - Previous Value Tracking ⭐⭐

jsx
/**
 * 🎯 Use case: So sánh giá trị hiện tại với giá trị trước đó
 * 💼 Real-world: Analytics, change detection, diff calculation
 */

import { useEffect, useRef, useState } from 'react';

export default function PriceTracker() {
  const [price, setPrice] = useState(100);
  const previousPrice = useRef(price);

  // DERIVED VALUES
  // Calculate change
  const priceChange = price - previousPrice.current;
  const changePercent = ((priceChange / previousPrice.current) * 100).toFixed(
    2,
  );

  // SIDE EFFECT (Commit phase → After render)
  // Chạy SAU khi DOM đã render xong
  // Lưu lại snapshot giá hiện tại cho LẦN RENDER TIẾP THEO
  useEffect(() => {
    previousPrice.current = price;
  }, [price]); // Chỉ update khi price thay đổi

  // Random price change (demo purpose)
  const updatePrice = () => {
    const change = Math.random() * 20 - 10; // -10 to +10
    setPrice((prev) => Math.max(1, prev + change)); // Không âm
  };

  /*
 * 1. Render
   - price = NEW
   - previousPrice = OLD

  2. Paint UI

  3. useEffect chạy
   - previousPrice = price (NEW)
*/

  return (
    <div style={{ padding: '20px', border: '1px solid #ccc' }}>
      <h2>Stock Price Tracker</h2>

      <div style={{ fontSize: '32px', fontWeight: 'bold' }}>
        ${price.toFixed(2)}
      </div>

      <div
        style={{
          color: priceChange >= 0 ? 'green' : 'red',
          fontSize: '18px',
        }}
      >
        {priceChange >= 0 ? '▲' : '▼'}${Math.abs(priceChange).toFixed(2)}(
        {changePercent}%)
      </div>

      <div style={{ marginTop: '10px', color: '#666' }}>
        Previous: ${previousPrice.current.toFixed(2)}
      </div>

      <button
        onClick={updatePrice}
        style={{ marginTop: '10px' }}
      >
        Update Price
      </button>
    </div>
  );
}

Tại sao dùng useRef thay vì useState?

jsx
// ❌ Cách 1: Dùng useState (không tốt)
function BadPriceTracker() {
  const [price, setPrice] = useState(100);
  const [previousPrice, setPreviousPrice] = useState(100);

  const updatePrice = () => {
    setPreviousPrice(price); // ⚠️ Gây thêm 1 re-render!
    setPrice(newPrice); // ⚠️ Lại 1 re-render!
    // → 2 renders thay vì 1!
  };

  return (
    <div>
      <p>Current: ${price}</p>
      <p>Previous: ${previousPrice}</p>
    </div>
  );
}

// ✅ Cách 2: Dùng useRef (tốt)
function GoodPriceTracker() {
  const [price, setPrice] = useState(100);
  const previousPrice = useRef(100);

  useEffect(() => {
    previousPrice.current = price; // Không gây re-render
  }, [price]);

  const updatePrice = () => {
    setPrice(newPrice); // Chỉ 1 re-render!
  };

  return (
    <div>
      <p>Current: ${price}</p>
      <p>Previous: ${previousPrice.current}</p>
    </div>
  );
}

Timeline so sánh:

useState approach:
──────────────────────────────────────
Initial render → price: 100, prev: 100
Click button:
  1. setPreviousPrice(100) → Render #2
  2. setPrice(120) → Render #3
→ 2 unnecessary renders!

useRef approach:
──────────────────────────────────────
Initial render → price: 100, prev.current: 100
Click button:
  1. setPrice(120) → Render #2
  2. useEffect: prev.current = 120 (no render)
→ Only 1 necessary render!

Demo 3: Edge Cases - Timer Management ⭐⭐⭐

jsx
/**
 * 🎯 Use case: Timer có pause / resume chính xác
 * 🧠 Core idea:
 *    - Date.now() = nguồn thời gian duy nhất (single source of truth)
 *    - setInterval chỉ dùng để trigger re-render
 *    - useRef giữ mutable data xuyên render
 */

import { useEffect, useRef, useState } from 'react';

export default function AccurateTimer() {
  // time = số GIÂY đã trôi qua (derived từ Date.now)
  const [time, setTime] = useState(0);
  const [isRunning, setIsRunning] = useState(false);

  // Lưu interval ID để tránh start nhiều lần
  const intervalRef = useRef(null);

  // Lưu "thời điểm bắt đầu giả định"
  // → dùng để tính thời gian trôi qua (elapsed time) chính xác
  const startTimeRef = useRef(null);

  const startTimer = () => {
    // ⚠️ Prevent multiple intervals
    if (intervalRef.current) return;

    // Khi Resume : `time` hiện tại là số giây đã chạy trước đó.
    // Ví dụ hiện tại 10:00 , đã chạy 5s rồi pause
    // Date.now() trả về Milliseconds nên cần đổi `time` sang đơn vị Milliseconds ( * 1000 )
    // => Timer bắt đầu từ 10:00 - 5s = 9:59:55, lưu vào `startTimeRef`
    startTimeRef.current = Date.now() - time * 1000;

    setIsRunning(true);

    /**
     * ❌ KHÔNG dùng:
     *    setTime(prev => prev + 1)
     *
     * ✅ LÝ DO:
     * 1. setInterval KHÔNG chính xác 1000ms
     * 2. Tab background → delay (Browser sẽ chủ động làm chậm / tạm ngưng timer để tiết kiệm tài nguyên khi chuyển tab khác / lock màn hình /...)
     * 3. Drift theo thời gian ( càng chạy lâu càng lệch xa thời gian thật )
     *
     * 👉 Thay vào đó:
     * - Tính lại time từ Date.now()
     * - Mỗi tick là một phép "sync lại với thời gian thực"
     */
    intervalRef.current = setInterval(() => {
      // Thời gian trôi qua = Hiện tại - Bắt đầu.
      // Sau đó chia 1000 để đổi đơn vị từ ms -> s
      const elapsedSeconds = Math.floor(
        (Date.now() - startTimeRef.current) / 1000,
      );

      setTime(elapsedSeconds);
    }, 100); // Tick nhanh để UI mượt, logic vẫn chuẩn
  };

  /* =====================================================
   * STOP TIMER (Pause)
   * ===================================================== */
  const stopTimer = () => {
    if (!intervalRef.current) return;

    clearInterval(intervalRef.current);
    intervalRef.current = null;
    setIsRunning(false);
  };

  /* =====================================================
   * RESET TIMER
   * ===================================================== */
  const resetTimer = () => {
    stopTimer();
    setTime(0);
    startTimeRef.current = null;
  };

  /* =====================================================
   * CLEANUP — Chạy khi component UNMOUNT - Dọn dẹp lần cuối
   * ===================================================== */
  useEffect(() => {
    return () => {
      // Prevent memory leak -  Ngăn chặn rò rỉ bộ nhớ
      // Trường hợp đang chạy timer mà tắt trình duyệt đột ngột
      if (intervalRef.current) {
        clearInterval(intervalRef.current);
      }
    };
  }, []);

  /* =====================================================
   * UI HELPERS
   * ===================================================== */
  const formatTime = (seconds) => {
    const mins = Math.floor(seconds / 60);
    const secs = seconds % 60;
    return `${mins.toString().padStart(2, '0')}:${secs
      .toString()
      .padStart(2, '0')}`;
  };

  return (
    <div style={{ padding: 20, textAlign: 'center' }}>
      <h2>Accurate Timer</h2>

      <div
        style={{
          fontSize: 48,
          fontFamily: 'monospace',
          margin: '20px 0',
        }}
      >
        {formatTime(time)}
      </div>

      <div style={{ display: 'flex', gap: 10, justifyContent: 'center' }}>
        <button
          onClick={startTimer}
          disabled={isRunning}
        >
          Start
        </button>

        <button
          onClick={stopTimer}
          disabled={!isRunning}
        >
          Stop
        </button>

        <button onClick={resetTimer}>Reset</button>
      </div>

      {/* Debug / teaching purpose */}
      <div style={{ marginTop: 20, fontSize: 12, color: '#666' }}>
        <p>Running: {isRunning ? 'Yes' : 'No'}</p>
        <p>Interval: {intervalRef.current ? 'Active' : 'null'}</p>
        <p>
          Start time:{' '}
          {startTimeRef.current
            ? new Date(startTimeRef.current).toLocaleTimeString()
            : 'null'}
        </p>
      </div>
    </div>
  );
}

Edge Cases được handle:

jsx
// ⚠️ Edge Case 1: Spam start button
const startTimer = () => {
  if (intervalRef.current) {
    // ✅ Prevent multiple intervals
    console.warn('Timer is already running!');
    return;
  }
  // ... start logic
};

// ⚠️ Edge Case 2: Component unmount khi timer running
useEffect(() => {
  return () => {
    if (intervalRef.current) {
      // ✅ Cleanup để tránh memory leak
      clearInterval(intervalRef.current);
    }
  };
}, []);

// ⚠️ Edge Case 3: Reset khi đang chạy
const resetTimer = () => {
  stopTimer(); // ✅ Stop trước khi reset
  setTime(0);
  startTimeRef.current = null;
};

Common Mistakes:

jsx
// ❌ Mistake 1: Không check timer đã running
const badStart = () => {
  // Không check → tạo nhiều intervals!
  intervalRef.current = setInterval(/* ... */);
};

// ❌ Mistake 2: Không cleanup
// Không có useEffect cleanup → memory leak khi unmount!

// ❌ Mistake 3: Clear wrong interval
const badStop = () => {
  clearInterval(intervalRef.current);
  // ⚠️ Quên set null → ref vẫn giữ invalid ID
};

// ✅ ĐÚNG:
const goodStop = () => {
  clearInterval(intervalRef.current);
  intervalRef.current = null; // Reset ref
};

🔨 PHẦN 3: BÀI TẬP THỰC HÀNH (60 phút)

⭐ Exercise 1: Click Counter với Previous Value (15 phút)

jsx
/**
 * 🎯 Mục tiêu: Thực hành useRef cơ bản với previous value tracking
 * ⏱️ Thời gian: 15 phút
 * 🚫 KHÔNG dùng: useReducer, useContext, custom hooks
 *
 * Requirements:
 * 1. Hiển thị số lần click hiện tại
 * 2. Hiển thị số lần click trước đó
 * 3. Hiển thị difference (current - previous)
 * 4. Button reset về 0
 *
 * 💡 Gợi ý:
 * - Dùng useState cho click count (UI data)
 * - Dùng useRef cho previous count (non-UI data)
 * - useEffect để update previous sau mỗi click
 */

// ❌ Cách SAI: Dùng 2 useState
function BadClickCounter() {
  const [count, setCount] = useState(0);
  const [prevCount, setPrevCount] = useState(0);

  const handleClick = () => {
    setPrevCount(count); // ⚠️ Extra re-render!
    setCount(count + 1); // ⚠️ Another re-render!
  };

  return (
    <div>
      <p>Current: {count}</p>
      <p>Previous: {prevCount}</p>
      <button onClick={handleClick}>Click Me</button>
    </div>
  );
}

// ✅ Cách ĐÚNG: useState + useRef
function GoodClickCounter() {
  // TODO: Implement using useState + useRef
  // Step 1: Create state for current count
  // Step 2: Create ref for previous count
  // Step 3: useEffect to update previous after count changes
  // Step 4: Calculate difference
  // Step 5: Reset button
}

// 🎯 NHIỆM VỤ CỦA BẠN:
function ClickCounter() {
  // TODO: Your implementation here

  return (
    <div style={{ padding: '20px', border: '2px solid #333' }}>
      <h2>Click Counter</h2>

      {/* TODO: Display current count */}
      {/* TODO: Display previous count */}
      {/* TODO: Display difference with color (green if +, red if -) */}

      {/* TODO: Click button */}
      {/* TODO: Reset button */}
    </div>
  );
}

// ✅ Expected behavior:
// Initial: Current: 0, Previous: 0, Diff: 0
// Click 1: Current: 1, Previous: 0, Diff: +1 (green)
// Click 2: Current: 2, Previous: 1, Diff: +1 (green)
// Reset: Current: 0, Previous: 2, Diff: -2 (red)
💡 Solution
jsx
/**
 * Click Counter with Previous Value Tracking
 * @returns {JSX.Element}
 */
function ClickCounter() {
  const [count, setCount] = useState(0);
  const prevCountRef = useRef(0);

  useEffect(() => {
    prevCountRef.current = count;
  }, [count]);

  const difference = count - prevCountRef.current;

  const handleReset = () => {
    setCount(0);
    // prevCountRef sẽ được cập nhật tự động qua useEffect
  };

  return (
    <div style={{ padding: '20px', border: '2px solid #333' }}>
      <h2>Click Counter</h2>

      <p>Current: {count}</p>
      <p>Previous: {prevCountRef.current}</p>

      <p
        style={{
          color: difference > 0 ? 'green' : difference < 0 ? 'red' : 'inherit',
        }}
      >
        Difference: {difference > 0 ? '+' : ''}
        {difference}
      </p>

      <div style={{ marginTop: '16px' }}>
        <button
          onClick={() => setCount((c) => c + 1)}
          style={{ marginRight: '12px' }}
        >
          Click Me
        </button>

        <button
          onClick={handleReset}
          style={{
            backgroundColor: '#dc3545',
            color: 'white',
          }}
        >
          Reset
        </button>
      </div>
    </div>
  );
}

/*
Kết quả mong đợi:

Initial: 
  Current: 0
  Previous: 0
  Difference: 0

Sau 1 lần click:
  Current: 1
  Previous: 0
  Difference: +1 (màu xanh)

Sau 2 lần click:
  Current: 2
  Previous: 1
  Difference: +1 (màu xanh)

Sau khi Reset:
  Current: 0
  Previous: 2
  Difference: -2 (màu đỏ)
*/

⭐⭐ Exercise 2: Debounced Search Input (25 phút)

jsx
/**
 * 🎯 Mục tiêu: Hiểu khi nào dùng useRef vs useState
 * ⏱️ Thời gian: 25 phút
 *
 * Scenario:
 * Bạn đang build search box. Mỗi lần user type, bạn muốn gọi API.
 * Nhưng KHÔNG muốn gọi API mỗi keystroke → dùng debounce.
 *
 * 🤔 PHÂN TÍCH:
 *
 * Approach A: Dùng setTimeout trong useEffect, store timeout ID trong useState
 * Pros:
 * - Straightforward
 * Cons:
 * - setState gây unnecessary re-render
 * - Performance waste
 *
 * Approach B: Dùng setTimeout trong useEffect, store timeout ID trong useRef
 * Pros:
 * - No unnecessary re-renders
 * - Better performance
 * - Timeout ID là non-UI data
 * Cons:
 * - None (this is the correct approach!)
 *
 * 💭 BẠN CHỌN GÌ VÀ TẠI SAO?
 * Document quyết định của bạn, sau đó implement.
 */

import { useState, useRef, useEffect } from 'react';

function DebouncedSearch() {
  const [searchTerm, setSearchTerm] = useState('');
  const [searchResults, setSearchResults] = useState([]);
  const [isSearching, setIsSearching] = useState(false);

  // TODO: Decide - useState hay useRef cho timeout ID?
  // const [timeoutId, setTimeoutId] = useState(null); // Approach A?
  // const timeoutRef = useRef(null); // Approach B?

  // TODO: Implement debounced search
  // Requirements:
  // 1. Wait 500ms after user stops typing
  // 2. Then "search" (mock với setTimeout)
  // 3. Show loading state while searching
  // 4. Display results
  // 5. Clear previous timeout khi user types again

  useEffect(() => {
    // TODO: Your debounce logic here

    // Pattern structure:
    // 1. Clear previous timeout (if exists)
    // 2. If searchTerm empty → clear results, return
    // 3. Set new timeout:
    //    - After 500ms: setIsSearching(true)
    //    - Mock API call (setTimeout 1000ms)
    //    - setSearchResults, setIsSearching(false)

    return () => {
      // TODO: Cleanup
    };
  }, [searchTerm]);

  // Mock search function
  const mockSearch = (term) => {
    return new Promise((resolve) => {
      setTimeout(() => {
        // Fake results
        const results = [
          `Result 1 for "${term}"`,
          `Result 2 for "${term}"`,
          `Result 3 for "${term}"`,
        ];
        resolve(results);
      }, 1000);
    });
  };

  return (
    <div style={{ padding: '20px' }}>
      <h2>Debounced Search</h2>

      <input
        type='text'
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
        placeholder='Type to search...'
        style={{
          width: '300px',
          padding: '10px',
          fontSize: '16px',
        }}
      />

      {isSearching && <p>Searching...</p>}

      <ul>
        {searchResults.map((result, index) => (
          <li key={index}>{result}</li>
        ))}
      </ul>

      {/* Debug info */}
      <div style={{ marginTop: '20px', fontSize: '12px', color: '#666' }}>
        <p>Search term: "{searchTerm}"</p>
        <p>Results count: {searchResults.length}</p>
      </div>
    </div>
  );
}

// 🎯 Expected behavior:
// - Type "react" → wait → see "Searching..." → see results
// - Type "react hooks" → debounce cancels first search → only search "react hooks"
// - Clear input → results disappear immediately
💡 Solution
jsx
/**
 * Debounced Search Input
 * Demonstrates proper use of useRef for timeout management to avoid unnecessary re-renders
 * @returns {JSX.Element}
 */
function DebouncedSearch() {
  const [searchTerm, setSearchTerm] = useState('');
  const [searchResults, setSearchResults] = useState([]);
  const [isSearching, setIsSearching] = useState(false);

  // Use useRef → timeout ID is not UI data & changing it shouldn't cause re-render
  const timeoutRef = useRef(null);

  useEffect(() => {
    // 1. Clear any existing timeout (previous search)
    if (timeoutRef.current) {
      clearTimeout(timeoutRef.current);
      timeoutRef.current = null;
    }

    // 2. If search term is empty → clear results immediately
    if (!searchTerm.trim()) {
      setSearchResults([]);
      setIsSearching(false);
      return;
    }

    // 3. Set new debounced timeout
    setIsSearching(true);

    timeoutRef.current = setTimeout(async () => {
      try {
        const results = await mockSearch(searchTerm);
        setSearchResults(results);
      } catch (err) {
        console.error('Search failed:', err);
        setSearchResults([]);
      } finally {
        setIsSearching(false);
        timeoutRef.current = null;
      }
    }, 500);

    // 4. Cleanup: clear timeout when effect re-runs or component unmounts
    return () => {
      if (timeoutRef.current) {
        clearTimeout(timeoutRef.current);
        timeoutRef.current = null;
      }
    };
  }, [searchTerm]);

  // Mock search function (simulates API delay)
  const mockSearch = (term) => {
    return new Promise((resolve) => {
      setTimeout(() => {
        const results = [
          `Result 1 for "${term}"`,
          `Result 2 for "${term}"`,
          `Result 3 for "${term}"`,
          `Result 4 for "${term}"`,
        ];
        resolve(results);
      }, 1000);
    });
  };

  return (
    <div style={{ padding: '20px' }}>
      <h2>Debounced Search</h2>

      <input
        type='text'
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
        placeholder='Type to search...'
        style={{
          width: '300px',
          padding: '10px',
          fontSize: '16px',
        }}
      />

      {isSearching && (
        <p style={{ color: '#007bff', marginTop: '12px' }}>Searching...</p>
      )}

      {searchResults.length > 0 && (
        <ul style={{ marginTop: '16px', paddingLeft: '20px' }}>
          {searchResults.map((result, index) => (
            <li
              key={index}
              style={{ marginBottom: '8px' }}
            >
              {result}
            </li>
          ))}
        </ul>
      )}

      {searchTerm && searchResults.length === 0 && !isSearching && (
        <p style={{ color: '#666', marginTop: '12px' }}>No results found</p>
      )}

      {/* Debug info */}
      <div style={{ marginTop: '30px', fontSize: '13px', color: '#555' }}>
        <p>Current search term: "{searchTerm}"</p>
        <p>Results count: {searchResults.length}</p>
        <p>Timeout active: {timeoutRef.current ? 'Yes' : 'No'}</p>
      </div>
    </div>
  );
}

/*
Expected behavior:

• Type "react" slowly → after ~500ms debounce → shows "Searching..." → after ~1s shows 4 results
• Type quickly "react hooks" → previous timeout is cancelled → only one search for "react hooks" runs
• Delete all text → results disappear immediately (no loading state)
• Type → stop for >500ms → search triggers
• Rapid typing → only the last term (after pause) triggers search
*/

⭐⭐⭐ Exercise 3: Stopwatch với Lap Times (40 phút)

jsx
/**
 * 🎯 Mục tiêu: Kịch bản thực tế với multiple refs
 * ⏱️ Thời gian: 40 phút
 *
 * 📋 Product Requirements:
 * User Story: "Là một runner, tôi muốn track lap times để phân tích performance"
 *
 * ✅ Acceptance Criteria:
 * - [ ] Start/Stop stopwatch
 * - [ ] Record lap times (array)
 * - [ ] Display current lap time
 * - [ ] Display all lap times với fastest/slowest highlight
 * - [ ] Reset clears everything
 * - [ ] Precise timing (use Date.now() not interval count)
 *
 * 🎨 Technical Constraints:
 * - Chỉ dùng useState, useRef, useEffect (Ngày 11-21)
 * - KHÔNG dùng useReducer (chưa học)
 * - KHÔNG dùng custom hooks (chưa học deep dive)
 *
 * 🚨 Edge Cases cần handle:
 * - Click Start nhiều lần
 * - Click Lap khi chưa start
 * - Component unmount khi stopwatch đang chạy
 * - Lap khi stopwatch đang stop
 *
 * 📝 Implementation Checklist:
 * - [ ] Core stopwatch functionality
 * - [ ] Lap recording
 * - [ ] Precise timing calculation
 * - [ ] Fastest/Slowest lap detection
 * - [ ] Edge cases handling
 * - [ ] Cleanup on unmount
 */

import { useState, useRef, useEffect } from 'react';

function Stopwatch() {
  // TODO: State for laps array
  const [laps, setLaps] = useState([]);

  // TODO: State for UI
  const [isRunning, setIsRunning] = useState(false);
  const [currentTime, setCurrentTime] = useState(0);

  // TODO: Refs for timing
  // - intervalRef: store interval ID
  // - startTimeRef: store start timestamp
  // - lastLapTimeRef: store last lap timestamp

  // TODO: Implement start
  const handleStart = () => {
    // Edge case: already running?
  };

  // TODO: Implement stop
  const handleStop = () => {
    // Clear interval, keep time
  };

  // TODO: Implement lap
  const handleLap = () => {
    // Edge case: not running?
    // Calculate lap time
    // Add to laps array
    // Update lastLapTimeRef
  };

  // TODO: Implement reset
  const handleReset = () => {
    // Stop if running
    // Clear all state
    // Reset all refs
  };

  // TODO: Calculate fastest/slowest
  const getFastestLap = () => {
    // Return index of fastest lap
  };

  const getSlowestLap = () => {
    // Return index of slowest lap
  };

  // TODO: Cleanup
  useEffect(() => {
    return () => {
      // Clear interval on unmount
    };
  }, []);

  // Format time helper
  const formatTime = (ms) => {
    const totalSeconds = Math.floor(ms / 1000);
    const minutes = Math.floor(totalSeconds / 60);
    const seconds = totalSeconds % 60;
    const milliseconds = Math.floor((ms % 1000) / 10);

    return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}.${milliseconds.toString().padStart(2, '0')}`;
  };

  return (
    <div style={{ padding: '20px', maxWidth: '400px', margin: '0 auto' }}>
      <h2>Stopwatch with Laps</h2>

      {/* Main timer display */}
      <div
        style={{
          fontSize: '48px',
          fontFamily: 'monospace',
          textAlign: 'center',
          margin: '20px 0',
          padding: '20px',
          backgroundColor: '#f0f0f0',
          borderRadius: '8px',
        }}
      >
        {formatTime(currentTime)}
      </div>

      {/* Control buttons */}
      <div
        style={{
          display: 'flex',
          gap: '10px',
          justifyContent: 'center',
          marginBottom: '20px',
        }}
      >
        {/* TODO: Start/Stop button (toggle based on isRunning) */}
        {/* TODO: Lap button (disabled if not running) */}
        {/* TODO: Reset button */}
      </div>

      {/* Lap times list */}
      <div
        style={{
          maxHeight: '300px',
          overflowY: 'auto',
          border: '1px solid #ccc',
          borderRadius: '4px',
          padding: '10px',
        }}
      >
        <h3>Lap Times</h3>
        {laps.length === 0 ? (
          <p style={{ color: '#999', textAlign: 'center' }}>No laps yet</p>
        ) : (
          <ol style={{ padding: '0 0 0 20px' }}>
            {/* TODO: Map through laps */}
            {/* TODO: Highlight fastest (green) and slowest (red) */}
            {/* Format: "Lap 1: 00:05.23" */}
          </ol>
        )}
      </div>
    </div>
  );
}

// 🎯 Expected behavior:
// 1. Click Start → timer runs
// 2. Click Lap → records lap time, continues timing
// 3. Multiple laps → fastest is green, slowest is red
// 4. Click Stop → timer stops, can resume
// 5. Click Reset → everything clears
💡 Solution
jsx
/**
 * Stopwatch with Lap Times
 * Precise timing using Date.now(), multiple refs for timers, lap tracking, fastest/slowest highlighting
 * @returns {JSX.Element}
 */
function Stopwatch() {
  const [laps, setLaps] = useState([]);
  const [isRunning, setIsRunning] = useState(false);
  const [currentTime, setCurrentTime] = useState(0);

  // Refs for timing control
  const intervalRef = useRef(null);
  const startTimeRef = useRef(null);
  const lastLapTimeRef = useRef(null);

  // Start / Resume
  const handleStart = () => {
    if (isRunning) return; // Prevent multiple intervals

    setIsRunning(true);
    const now = Date.now();
    startTimeRef.current = now - currentTime; // Resume from current time
    lastLapTimeRef.current = lastLapTimeRef.current ?? now;

    intervalRef.current = setInterval(() => {
      setCurrentTime(Date.now() - startTimeRef.current);
    }, 10); // 10ms for smooth display
  };

  // Stop
  const handleStop = () => {
    if (!isRunning) return;

    setIsRunning(false);
    if (intervalRef.current) {
      clearInterval(intervalRef.current);
      intervalRef.current = null;
    }
    // Keep currentTime as is for resume
  };

  // Lap
  const handleLap = () => {
    if (!isRunning) return; // Can't lap when stopped

    const now = Date.now();
    const lapTime = now - (lastLapTimeRef.current || startTimeRef.current);
    lastLapTimeRef.current = now;

    setLaps((prev) => [
      ...prev,
      {
        time: lapTime,
        totalTime: currentTime,
      },
    ]);
  };

  // Reset
  const handleReset = () => {
    handleStop();
    setCurrentTime(0);
    setLaps([]);
    startTimeRef.current = null;
    lastLapTimeRef.current = null;
  };

  // Find fastest & slowest lap indices
  const getFastestLapIndex = () => {
    if (laps.length < 2) return -1;
    let min = laps[0].time;
    let index = 0;
    laps.forEach((lap, i) => {
      if (lap.time < min) {
        min = lap.time;
        index = i;
      }
    });
    return index;
  };

  const getSlowestLapIndex = () => {
    if (laps.length < 2) return -1;
    let max = laps[0].time;
    let index = 0;
    laps.forEach((lap, i) => {
      if (lap.time > max) {
        max = lap.time;
        index = i;
      }
    });
    return index;
  };

  const fastestIndex = getFastestLapIndex();
  const slowestIndex = getSlowestLapIndex();

  // Format time (mm:ss.ss)
  const formatTime = (ms) => {
    const totalSeconds = Math.floor(ms / 1000);
    const minutes = Math.floor(totalSeconds / 60);
    const seconds = totalSeconds % 60;
    const centiseconds = Math.floor((ms % 1000) / 10);
    return `${minutes.toString().padStart(2, '0')}:${seconds
      .toString()
      .padStart(2, '0')}.${centiseconds.toString().padStart(2, '0')}`;
  };

  // Cleanup on unmount
  useEffect(() => {
    return () => {
      if (intervalRef.current) {
        clearInterval(intervalRef.current);
      }
    };
  }, []);

  return (
    <div style={{ padding: '20px', maxWidth: '400px', margin: '0 auto' }}>
      <h2>Stopwatch with Laps</h2>

      {/* Main timer */}
      <div
        style={{
          fontSize: '48px',
          fontFamily: 'monospace',
          textAlign: 'center',
          margin: '20px 0',
          padding: '20px',
          backgroundColor: '#f0f0f0',
          borderRadius: '8px',
        }}
      >
        {formatTime(currentTime)}
      </div>

      {/* Controls */}
      <div
        style={{
          display: 'flex',
          gap: '10px',
          justifyContent: 'center',
          marginBottom: '20px',
        }}
      >
        {isRunning ? (
          <button
            onClick={handleStop}
            style={{
              padding: '10px 20px',
              backgroundColor: '#dc3545',
              color: 'white',
              border: 'none',
              borderRadius: '4px',
            }}
          >
            Stop
          </button>
        ) : (
          <button
            onClick={handleStart}
            style={{
              padding: '10px 20px',
              backgroundColor: '#28a745',
              color: 'white',
              border: 'none',
              borderRadius: '4px',
            }}
          >
            {currentTime === 0 ? 'Start' : 'Resume'}
          </button>
        )}

        <button
          onClick={handleLap}
          disabled={!isRunning}
          style={{
            padding: '10px 20px',
            backgroundColor: '#007bff',
            color: 'white',
            border: 'none',
            borderRadius: '4px',
            opacity: isRunning ? 1 : 0.6,
          }}
        >
          Lap
        </button>

        <button
          onClick={handleReset}
          style={{
            padding: '10px 20px',
            backgroundColor: '#6c757d',
            color: 'white',
            border: 'none',
            borderRadius: '4px',
          }}
        >
          Reset
        </button>
      </div>

      {/* Lap list */}
      <div
        style={{
          maxHeight: '300px',
          overflowY: 'auto',
          border: '1px solid #ccc',
          borderRadius: '4px',
          padding: '10px',
        }}
      >
        <h3>Lap Times</h3>
        {laps.length === 0 ? (
          <p style={{ color: '#999', textAlign: 'center' }}>No laps yet</p>
        ) : (
          <ol style={{ padding: '0 0 0 20px', margin: 0 }}>
            {laps.map((lap, index) => {
              const isFastest = index === fastestIndex;
              const isSlowest = index === slowestIndex;
              const style = {
                color: isFastest ? 'green' : isSlowest ? 'red' : 'inherit',
                fontWeight: isFastest || isSlowest ? 'bold' : 'normal',
              };
              return (
                <li
                  key={index}
                  style={style}
                >
                  Lap {index + 1}: {formatTime(lap.time)} (Total:{' '}
                  {formatTime(lap.totalTime)})
                </li>
              );
            })}
          </ol>
        )}
      </div>
    </div>
  );
}

/*
Expected behavior:

1. Click Start → timer runs smoothly
2. Click Lap → records current lap time, continues timing
3. Multiple laps → fastest lap is green, slowest is red
4. Click Stop → timer pauses, can resume
5. Click Reset → timer and laps clear
6. Edge cases handled:
   - Cannot lap when stopped
   - Cannot start multiple intervals
   - Cleanup on unmount prevents memory leak
   - Precise timing using Date.now()
*/

⭐⭐⭐⭐ Exercise 4: Form with Auto-save (60 phút)

jsx
/**
 * 🎯 Mục tiêu: Architectural decision với multiple approaches
 * ⏱️ Thời gian: 60 phút
 *
 * 🏗️ PHASE 1: Research & Design (20 phút)
 *
 * Scenario:
 * Build form tự động save draft sau khi user stop typing 2 giây.
 * Hiển thị last saved time và saving status.
 *
 * Nhiệm vụ:
 * 1. So sánh ít nhất 3 approaches:
 *    - Approach A: All useState (including timeout ID)
 *    - Approach B: useState + useRef for timeout
 *    - Approach C: useState + useRef for timeout + last save time
 * 2. Document pros/cons mỗi approach
 * 3. Chọn approach phù hợp nhất
 * 4. Viết ADR (Architecture Decision Record)
 *
 * ADR Template:
 * ────────────────────────────────────────
 * Context:
 * - Form có nhiều fields
 * - Auto-save sau 2s không type
 * - Hiển thị saving status
 * - Hiển thị last saved time
 *
 * Decision: [Approach bạn chọn]
 *
 * Rationale:
 * - [Lý do 1]
 * - [Lý do 2]
 * - [Lý do 3]
 *
 * Consequences:
 * Trade-offs accepted:
 * - [Trade-off 1]
 * - [Trade-off 2]
 *
 * Alternatives Considered:
 * - Approach A: [Brief summary + why rejected]
 * - Approach B: [Brief summary + why rejected]
 * ────────────────────────────────────────
 *
 * 💻 PHASE 2: Implementation (30 phút)
 */

import { useState, useRef, useEffect } from 'react';

function AutoSaveForm() {
  // Form data
  const [formData, setFormData] = useState({
    title: '',
    content: '',
    category: 'general',
  });

  // UI states
  const [savingStatus, setSavingStatus] = useState('idle'); // 'idle' | 'saving' | 'saved'
  const [lastSavedAt, setLastSavedAt] = useState(null);

  // TODO: Decide architecture
  // What refs do you need?
  // - Timeout ID?
  // - Last saved data (để compare)?
  // - Save timestamp?

  // TODO: Implement auto-save logic
  useEffect(() => {
    // Debounce logic (2 seconds)
    // 1. Clear previous timeout
    // 2. Set new timeout
    // 3. Compare current data với last saved
    // 4. If different → save
    // 5. Update saving status

    return () => {
      // Cleanup
    };
  }, [formData]);

  // Mock save function
  const saveDraft = async (data) => {
    return new Promise((resolve) => {
      setTimeout(() => {
        console.log('Saved:', data);
        resolve();
      }, 1000);
    });
  };

  const handleChange = (field) => (e) => {
    setFormData((prev) => ({
      ...prev,
      [field]: e.target.value,
    }));
  };

  // Manual save
  const handleManualSave = async () => {
    setSavingStatus('saving');
    await saveDraft(formData);
    setSavingStatus('saved');
    setLastSavedAt(new Date());
  };

  // Format last saved time
  const formatLastSaved = () => {
    if (!lastSavedAt) return 'Never';

    const now = Date.now();
    const diff = now - lastSavedAt.getTime();
    const seconds = Math.floor(diff / 1000);

    if (seconds < 60) return `${seconds} seconds ago`;
    const minutes = Math.floor(seconds / 60);
    if (minutes < 60) return `${minutes} minutes ago`;
    return lastSavedAt.toLocaleTimeString();
  };

  return (
    <div style={{ padding: '20px', maxWidth: '600px', margin: '0 auto' }}>
      <h2>Auto-save Form</h2>

      {/* Saving status indicator */}
      <div
        style={{
          padding: '10px',
          marginBottom: '20px',
          backgroundColor:
            savingStatus === 'saving'
              ? '#fff3cd'
              : savingStatus === 'saved'
                ? '#d1e7dd'
                : '#f8f9fa',
          border: '1px solid',
          borderColor:
            savingStatus === 'saving'
              ? '#ffc107'
              : savingStatus === 'saved'
                ? '#28a745'
                : '#dee2e6',
          borderRadius: '4px',
          display: 'flex',
          justifyContent: 'space-between',
          alignItems: 'center',
        }}
      >
        <span>
          {savingStatus === 'saving' && '💾 Saving...'}
          {savingStatus === 'saved' && '✅ Saved'}
          {savingStatus === 'idle' && '📝 Draft'}
        </span>
        <span style={{ fontSize: '12px', color: '#666' }}>
          Last saved: {formatLastSaved()}
        </span>
      </div>

      {/* Form fields */}
      <div style={{ marginBottom: '20px' }}>
        <label
          style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}
        >
          Title
        </label>
        <input
          type='text'
          value={formData.title}
          onChange={handleChange('title')}
          placeholder='Enter title...'
          style={{
            width: '100%',
            padding: '10px',
            fontSize: '16px',
            border: '1px solid #ccc',
            borderRadius: '4px',
          }}
        />
      </div>

      <div style={{ marginBottom: '20px' }}>
        <label
          style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}
        >
          Content
        </label>
        <textarea
          value={formData.content}
          onChange={handleChange('content')}
          placeholder='Enter content...'
          rows={6}
          style={{
            width: '100%',
            padding: '10px',
            fontSize: '16px',
            border: '1px solid #ccc',
            borderRadius: '4px',
            fontFamily: 'inherit',
          }}
        />
      </div>

      <div style={{ marginBottom: '20px' }}>
        <label
          style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}
        >
          Category
        </label>
        <select
          value={formData.category}
          onChange={handleChange('category')}
          style={{
            width: '100%',
            padding: '10px',
            fontSize: '16px',
            border: '1px solid #ccc',
            borderRadius: '4px',
          }}
        >
          <option value='general'>General</option>
          <option value='work'>Work</option>
          <option value='personal'>Personal</option>
        </select>
      </div>

      <button
        onClick={handleManualSave}
        disabled={savingStatus === 'saving'}
        style={{
          padding: '10px 20px',
          fontSize: '16px',
          backgroundColor: '#007bff',
          color: 'white',
          border: 'none',
          borderRadius: '4px',
          cursor: savingStatus === 'saving' ? 'not-allowed' : 'pointer',
          opacity: savingStatus === 'saving' ? 0.6 : 1,
        }}
      >
        Save Now
      </button>

      {/* Debug info */}
      <div
        style={{
          marginTop: '30px',
          padding: '10px',
          backgroundColor: '#f8f9fa',
          borderRadius: '4px',
          fontSize: '12px',
          fontFamily: 'monospace',
        }}
      >
        <p>
          <strong>Debug Info:</strong>
        </p>
        <p>Status: {savingStatus}</p>
        <p>Form Data: {JSON.stringify(formData)}</p>
      </div>
    </div>
  );
}

// 🧪 PHASE 3: Testing (10 phút)
// Manual testing checklist:
// - [ ] Type in title → waits 2s → auto-saves
// - [ ] Type quickly → only saves once after stop typing
// - [ ] Change category → auto-saves
// - [ ] Click "Save Now" → saves immediately
// - [ ] Status indicator updates correctly
// - [ ] Last saved time updates
// - [ ] No unnecessary re-renders (check React DevTools)
💡 Solution
jsx
/**
 * Auto-save Form with Debounced Saving
 * Uses useRef for timeout management to prevent unnecessary re-renders
 * Shows saving status and last saved timestamp
 * @returns {JSX.Element}
 */
function AutoSaveForm() {
  const [formData, setFormData] = useState({
    title: '',
    content: '',
    category: 'general',
  });

  const [savingStatus, setSavingStatus] = useState('idle'); // 'idle' | 'saving' | 'saved'
  const [lastSavedAt, setLastSavedAt] = useState(null);

  // Refs for non-UI mutable values
  const timeoutRef = useRef(null);
  const lastSavedDataRef = useRef(null); // to compare and avoid unnecessary saves

  // Mock save function
  const saveDraft = async (data) => {
    return new Promise((resolve) => {
      setTimeout(() => {
        console.log('Saved draft:', data);
        resolve();
      }, 1000);
    });
  };

  // Format time since last save
  const formatLastSaved = () => {
    if (!lastSavedAt) return 'Never';
    const diffMs = Date.now() - lastSavedAt;
    const seconds = Math.floor(diffMs / 1000);
    if (seconds < 60) return `${seconds} seconds ago`;
    const minutes = Math.floor(seconds / 60);
    if (minutes < 60) return `${minutes} minutes ago`;
    return new Date(lastSavedAt).toLocaleTimeString();
  };

  // Handle form changes
  const handleChange = (field) => (e) => {
    setFormData((prev) => ({
      ...prev,
      [field]: e.target.value,
    }));
  };

  // Manual save button
  const handleManualSave = async () => {
    setSavingStatus('saving');
    try {
      await saveDraft(formData);
      setLastSavedAt(Date.now());
      lastSavedDataRef.current = { ...formData };
      setSavingStatus('saved');
    } catch (err) {
      console.error('Manual save failed:', err);
      setSavingStatus('idle');
    }
  };

  // Auto-save logic with debounce
  useEffect(() => {
    // Clear any existing timeout
    if (timeoutRef.current) {
      clearTimeout(timeoutRef.current);
      timeoutRef.current = null;
    }

    // Skip save if data hasn't actually changed since last save
    if (
      lastSavedDataRef.current &&
      JSON.stringify(lastSavedDataRef.current) === JSON.stringify(formData)
    ) {
      return;
    }

    // Set new debounced save (2 seconds)
    timeoutRef.current = setTimeout(async () => {
      setSavingStatus('saving');

      try {
        await saveDraft(formData);
        setLastSavedAt(Date.now());
        lastSavedDataRef.current = { ...formData };
        setSavingStatus('saved');

        // Reset to idle after a short delay so user sees "saved" feedback
        setTimeout(() => {
          setSavingStatus('idle');
        }, 2000);
      } catch (err) {
        console.error('Auto-save failed:', err);
        setSavingStatus('idle');
      }
    }, 2000);

    // Cleanup on unmount or when formData changes
    return () => {
      if (timeoutRef.current) {
        clearTimeout(timeoutRef.current);
        timeoutRef.current = null;
      }
    };
  }, [formData]);

  return (
    <div style={{ padding: '20px', maxWidth: '600px', margin: '0 auto' }}>
      <h2>Auto-save Form</h2>

      {/* Status indicator */}
      <div
        style={{
          padding: '12px',
          marginBottom: '20px',
          backgroundColor:
            savingStatus === 'saving'
              ? '#fff3cd'
              : savingStatus === 'saved'
                ? '#d1e7dd'
                : '#f8f9fa',
          border: '1px solid',
          borderColor:
            savingStatus === 'saving'
              ? '#ffc107'
              : savingStatus === 'saved'
                ? '#28a745'
                : '#dee2e6',
          borderRadius: '6px',
          display: 'flex',
          justifyContent: 'space-between',
          alignItems: 'center',
        }}
      >
        <span style={{ fontWeight: '500' }}>
          {savingStatus === 'saving' && '💾 Saving...'}
          {savingStatus === 'saved' && '✅ Saved'}
          {savingStatus === 'idle' && '📝 Draft ready to save'}
        </span>
        <span style={{ fontSize: '14px', color: '#555' }}>
          Last saved: {formatLastSaved()}
        </span>
      </div>

      {/* Form fields */}
      <div style={{ marginBottom: '20px' }}>
        <label
          style={{ display: 'block', marginBottom: '6px', fontWeight: 'bold' }}
        >
          Title
        </label>
        <input
          type='text'
          value={formData.title}
          onChange={handleChange('title')}
          placeholder='Enter title...'
          style={{
            width: '100%',
            padding: '10px',
            fontSize: '16px',
            borderRadius: '4px',
            border: '1px solid #ccc',
          }}
        />
      </div>

      <div style={{ marginBottom: '20px' }}>
        <label
          style={{ display: 'block', marginBottom: '6px', fontWeight: 'bold' }}
        >
          Content
        </label>
        <textarea
          value={formData.content}
          onChange={handleChange('content')}
          placeholder='Enter content...'
          rows={6}
          style={{
            width: '100%',
            padding: '10px',
            fontSize: '16px',
            borderRadius: '4px',
            border: '1px solid #ccc',
            fontFamily: 'inherit',
          }}
        />
      </div>

      <div style={{ marginBottom: '24px' }}>
        <label
          style={{ display: 'block', marginBottom: '6px', fontWeight: 'bold' }}
        >
          Category
        </label>
        <select
          value={formData.category}
          onChange={handleChange('category')}
          style={{
            width: '100%',
            padding: '10px',
            fontSize: '16px',
            borderRadius: '4px',
            border: '1px solid #ccc',
          }}
        >
          <option value='general'>General</option>
          <option value='work'>Work</option>
          <option value='personal'>Personal</option>
        </select>
      </div>

      <button
        onClick={handleManualSave}
        disabled={savingStatus === 'saving'}
        style={{
          padding: '12px 24px',
          fontSize: '16px',
          backgroundColor: '#007bff',
          color: 'white',
          border: 'none',
          borderRadius: '6px',
          cursor: savingStatus === 'saving' ? 'not-allowed' : 'pointer',
          opacity: savingStatus === 'saving' ? 0.6 : 1,
        }}
      >
        Save Now
      </button>

      {/* Debug panel */}
      <div
        style={{
          marginTop: '40px',
          padding: '12px',
          backgroundColor: '#f8f9fa',
          borderRadius: '6px',
          fontSize: '13px',
          color: '#444',
        }}
      >
        <strong>Debug Info:</strong>
        <br />
        Status: {savingStatus}
        <br />
        Auto-save timeout active: {timeoutRef.current ? 'Yes' : 'No'}
        <br />
        Form data changed since last save:{' '}
        {lastSavedDataRef.current
          ? JSON.stringify(lastSavedDataRef.current) !==
            JSON.stringify(formData)
          : 'Yes (first save)'}
      </div>
    </div>
  );
}

/*
Expected behavior:

• Type in any field → after 2 seconds of inactivity → shows "Saving..." → then "Saved" → status returns to idle
• Rapid typing → only one save attempt after user stops for 2s
• Changing category also triggers auto-save
• Click "Save Now" → saves immediately, bypasses debounce
• If content doesn't change → no unnecessary save attempts
• Status indicator changes color appropriately
• Last saved time updates correctly
*/

⭐⭐⭐⭐⭐ Exercise 5: Advanced Polling Component (90 phút)

jsx
/**
 * 🎯 Mục tiêu: Production-ready component với complex ref management
 * ⏱️ Thời gian: 90 phút
 *
 * 📋 Feature Specification:
 *
 * Build một component fetch data từ API với:
 * 1. Tự động refresh theo chu kỳ X giây (có thể cấu hình)
 * 2. Hỗ trợ pause / resume polling
 * 3. Cho phép refresh thủ công
 * 4. Hiển thị thời gian kể từ lần cập nhật thành công gần nhất
 * 5. Hiển thị trạng thái kết nối (idle / loading / success / error)
 * 6. Tự động tạm dừng polling khi tab không active
 * 7. Retry request khi lỗi bằng exponential backoff
 * 8. Hỗ trợ huỷ request đang chạy (AbortController)
 *
 * 🏗️ Technical Design Doc:
 *
 * 1. Component Architecture:
 *    - State: data, loading, error, status, lastUpdate
 *    - Refs: intervalId, retryCount, abortController
 *    - Props: url, interval, onData, onError
 *
 * 2. State Management Strategy:
 *    - useState cho UI-critical data
 *    - useRef cho timers, counters, abort controllers
 *    - Không dùng useReducer (chưa học)
 *
 * 3. API Integration:
 *    - fetch với AbortController
 *    - Error handling với retry logic
 *    - Response validation
 *
 * 4. Performance Considerations:
 *    - Cleanup tất cả subscriptions
 *    - Cancel in-flight requests
 *    - Pause khi tab hidden
 *
 * 5. Error Handling Strategy:
 *    - Try exponential backoff: 1s, 2s, 4s, 8s, 16s
 *    - Max 5 retries
 *    - Reset retry count on success
 *    - Display error message
 *
 * ✅ Production Checklist:
 * - [ ] TypeScript types đầy đủ (ở đây dùng JSDoc comments)
 * - [ ] Comprehensive error handling
 * - [ ] Loading states
 * - [ ] Empty states
 * - [ ] Edge case handling (rapid pause/resume, unmount during fetch)
 * - [ ] Performance optimization (unnecessary re-renders)
 * - [ ] Memory leak prevention
 * - [ ] Visibility API integration
 * - [ ] Request cancellation
 * - [ ] Console logging cho debugging
 */

import { useState, useRef, useEffect } from 'react';

/**
 * @typedef {Object} PollingConfig
 * @property {string} url - API endpoint
 * @property {number} interval - Polling interval in ms (default: 5000)
 * @property {boolean} pauseOnHidden - Pause when tab hidden (default: true)
 * @property {Function} onData - Callback khi có data mới
 * @property {Function} onError - Callback khi có error
 */

/**
 * Advanced Polling Component
 * @param {PollingConfig} props
 */
function AdvancedPolling({
  url,
  interval = 5000,
  pauseOnHidden = true,
  onData,
  onError,
}) {
  // ═══════════════════════════════════════
  // STATE MANAGEMENT
  // ═══════════════════════════════════════

  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [status, setStatus] = useState('idle'); // 'idle' | 'polling' | 'paused' | 'error'
  const [lastUpdate, setLastUpdate] = useState(null);

  // ═══════════════════════════════════════
  // REFS FOR NON-UI DATA
  // ═══════════════════════════════════════

  // TODO: Implement refs
  // - intervalRef: store setInterval ID
  // - retryCountRef: track consecutive failures
  // - abortControllerRef: cancel requests
  // - isPausedRef: track pause state (sync with visibility)

  // ═══════════════════════════════════════
  // CORE FETCHING LOGIC
  // ═══════════════════════════════════════

  const fetchData = async () => {
    // TODO: Implement với:
    // 1. Check if paused
    // 2. Create AbortController
    // 3. setLoading(true)
    // 4. try-catch fetch
    // 5. Validate response
    // 6. Update data, lastUpdate, reset retryCount
    // 7. Call onData callback
    // 8. catch: handle errors, exponential backoff
    // 9. finally: setLoading(false)
  };

  // ═══════════════════════════════════════
  // POLLING CONTROL
  // ═══════════════════════════════════════

  const startPolling = () => {
    // TODO:
    // 1. Check if already polling
    // 2. Clear existing interval
    // 3. Fetch immediately
    // 4. Set up interval
    // 5. Update status
  };

  const pausePolling = () => {
    // TODO:
    // 1. Clear interval
    // 2. Update status
    // 3. Set isPausedRef
  };

  const resumePolling = () => {
    // TODO:
    // 1. Reset isPausedRef
    // 2. Start polling
  };

  const manualRefresh = () => {
    // TODO:
    // 1. Cancel current request if any
    // 2. Reset retry count
    // 3. Fetch immediately
  };

  // ═══════════════════════════════════════
  // VISIBILITY CHANGE HANDLING
  // ═══════════════════════════════════════

  useEffect(() => {
    if (!pauseOnHidden) return;

    // TODO: Implement Visibility API
    // 1. Add event listener for 'visibilitychange'
    // 2. If hidden → pause polling
    // 3. If visible → resume polling
    // 4. Cleanup listener

    return () => {
      // Cleanup
    };
  }, [pauseOnHidden]);

  // ═══════════════════════════════════════
  // INITIAL START & CLEANUP
  // ═══════════════════════════════════════

  useEffect(() => {
    // TODO:
    // 1. Start polling on mount
    // 2. Cleanup everything on unmount
    //    - Clear interval
    //    - Abort in-flight request

    return () => {
      // Critical cleanup
    };
  }, [url, interval]);

  // ═══════════════════════════════════════
  // HELPER: Calculate time since last update
  // ═══════════════════════════════════════

  const getTimeSinceUpdate = () => {
    if (!lastUpdate) return 'Never';

    const diff = Date.now() - lastUpdate;
    const seconds = Math.floor(diff / 1000);

    if (seconds < 60) return `${seconds}s ago`;
    const minutes = Math.floor(seconds / 60);
    if (minutes < 60) return `${minutes}m ago`;
    const hours = Math.floor(minutes / 60);
    return `${hours}h ago`;
  };

  // ═══════════════════════════════════════
  // HELPER: Get status color
  // ═══════════════════════════════════════

  const getStatusColor = () => {
    switch (status) {
      case 'polling':
        return '#28a745';
      case 'paused':
        return '#ffc107';
      case 'error':
        return '#dc3545';
      default:
        return '#6c757d';
    }
  };

  // ═══════════════════════════════════════
  // RENDER
  // ═══════════════════════════════════════

  return (
    <div
      style={{
        padding: '20px',
        maxWidth: '800px',
        margin: '0 auto',
        fontFamily: 'system-ui, -apple-system, sans-serif',
      }}
    >
      <h2>Advanced Polling Component</h2>

      {/* Status Bar */}
      <div
        style={{
          padding: '15px',
          marginBottom: '20px',
          backgroundColor: '#f8f9fa',
          border: '2px solid',
          borderColor: getStatusColor(),
          borderRadius: '8px',
          display: 'flex',
          justifyContent: 'space-between',
          alignItems: 'center',
        }}
      >
        <div>
          <span
            style={{
              display: 'inline-block',
              width: '10px',
              height: '10px',
              borderRadius: '50%',
              backgroundColor: getStatusColor(),
              marginRight: '10px',
            }}
          />
          <strong>Status:</strong> {status}
        </div>
        <div style={{ fontSize: '14px', color: '#666' }}>
          Last update: {getTimeSinceUpdate()}
        </div>
      </div>

      {/* Control Buttons */}
      <div
        style={{
          display: 'flex',
          gap: '10px',
          marginBottom: '20px',
        }}
      >
        {status === 'polling' ? (
          <button
            onClick={pausePolling}
            style={{
              padding: '10px 20px',
              backgroundColor: '#ffc107',
              color: '#000',
              border: 'none',
              borderRadius: '4px',
              cursor: 'pointer',
              fontWeight: 'bold',
            }}
          >
            ⏸ Pause
          </button>
        ) : (
          <button
            onClick={resumePolling}
            style={{
              padding: '10px 20px',
              backgroundColor: '#28a745',
              color: 'white',
              border: 'none',
              borderRadius: '4px',
              cursor: 'pointer',
              fontWeight: 'bold',
            }}
          >
            ▶ Resume
          </button>
        )}

        <button
          onClick={manualRefresh}
          disabled={loading}
          style={{
            padding: '10px 20px',
            backgroundColor: '#007bff',
            color: 'white',
            border: 'none',
            borderRadius: '4px',
            cursor: loading ? 'not-allowed' : 'pointer',
            opacity: loading ? 0.6 : 1,
            fontWeight: 'bold',
          }}
        >
          🔄 Refresh Now
        </button>
      </div>

      {/* Loading Indicator */}
      {loading && (
        <div
          style={{
            padding: '10px',
            backgroundColor: '#e3f2fd',
            border: '1px solid #2196f3',
            borderRadius: '4px',
            marginBottom: '20px',
            textAlign: 'center',
          }}
        >
          ⏳ Fetching data...
        </div>
      )}

      {/* Error Display */}
      {error && (
        <div
          style={{
            padding: '15px',
            backgroundColor: '#f8d7da',
            border: '1px solid #dc3545',
            borderRadius: '4px',
            marginBottom: '20px',
            color: '#721c24',
          }}
        >
          <strong>❌ Error:</strong> {error}
          <div style={{ marginTop: '10px', fontSize: '14px' }}>
            Retry attempt: {/* TODO: show retryCount */}
          </div>
        </div>
      )}

      {/* Data Display */}
      <div
        style={{
          padding: '20px',
          backgroundColor: 'white',
          border: '1px solid #dee2e6',
          borderRadius: '8px',
          minHeight: '200px',
        }}
      >
        <h3>Data:</h3>
        {data ? (
          <pre
            style={{
              backgroundColor: '#f8f9fa',
              padding: '15px',
              borderRadius: '4px',
              overflow: 'auto',
              fontSize: '14px',
            }}
          >
            {JSON.stringify(data, null, 2)}
          </pre>
        ) : (
          <p style={{ color: '#999', textAlign: 'center' }}>
            No data yet. Waiting for first poll...
          </p>
        )}
      </div>

      {/* Debug Info */}
      <details style={{ marginTop: '20px' }}>
        <summary style={{ cursor: 'pointer', fontWeight: 'bold' }}>
          🔍 Debug Information
        </summary>
        <div
          style={{
            marginTop: '10px',
            padding: '15px',
            backgroundColor: '#f8f9fa',
            borderRadius: '4px',
            fontSize: '12px',
            fontFamily: 'monospace',
          }}
        >
          <p>
            <strong>URL:</strong> {url}
          </p>
          <p>
            <strong>Interval:</strong> {interval}ms
          </p>
          <p>
            <strong>Status:</strong> {status}
          </p>
          <p>
            <strong>Loading:</strong> {loading ? 'Yes' : 'No'}
          </p>
          <p>
            <strong>Has Data:</strong> {data ? 'Yes' : 'No'}
          </p>
          <p>
            <strong>Has Error:</strong> {error ? 'Yes' : 'No'}
          </p>
          <p>
            <strong>Pause on Hidden:</strong> {pauseOnHidden ? 'Yes' : 'No'}
          </p>
          {/* TODO: Add more debug info from refs */}
        </div>
      </details>
    </div>
  );
}

// ═══════════════════════════════════════
// EXAMPLE USAGE
// ═══════════════════════════════════════

function App() {
  return (
    <AdvancedPolling
      url='https://jsonplaceholder.typicode.com/posts/1'
      interval={5000}
      pauseOnHidden={true}
      onData={(data) => console.log('New data:', data)}
      onError={(error) => console.error('Polling error:', error)}
    />
  );
}

// 📝 Documentation Requirements:
//
// Write a README.md explaining:
// 1. Component API (props)
// 2. Features
// 3. Usage examples
// 4. Edge cases handled
// 5. Performance considerations
//
// 🔍 Code Review Self-Checklist:
// - [ ] All refs properly initialized
// - [ ] All intervals/timeouts cleared
// - [ ] All requests cancellable
// - [ ] No memory leaks
// - [ ] Error handling comprehensive
// - [ ] Loading states accurate
// - [ ] Status updates correct
// - [ ] Visibility API working
// - [ ] Exponential backoff implemented
// - [ ] Console logs helpful for debugging
// - [ ] Code readable and maintainable
// - [ ] Edge cases handled
// - [ ] Comments explain complex logic
💡 Solution
jsx
/**
 * Advanced Polling Component
 * Features: auto-polling, pause/resume, manual refresh, visibility handling,
 *           exponential backoff on error, request cancellation
 * @param {Object} props
 * @param {string} props.url - API endpoint to poll
 * @param {number} [props.interval=5000] - Polling interval in ms
 * @param {boolean} [props.pauseOnHidden=true] - Pause polling when tab is hidden
 * @param {Function} [props.onData] - Optional callback when new data arrives
 * @param {Function} [props.onError] - Optional callback on error
 */
function AdvancedPolling({
  url,
  interval = 5000,
  pauseOnHidden = true,
  onData,
  onError,
}) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [status, setStatus] = useState('idle'); // idle | polling | paused | error

  const [lastUpdate, setLastUpdate] = useState(null);

  // Refs for mutable, non-UI values
  const intervalRef = useRef(null);
  const abortControllerRef = useRef(null);
  const retryCountRef = useRef(0);
  const isPausedRef = useRef(false);
  const isMountedRef = useRef(true);

  // Exponential backoff delays (in ms)
  const backoffDelays = [1000, 2000, 4000, 8000, 16000];

  const fetchData = async (isManual = false) => {
    if (isPausedRef.current && !isManual) return;

    // Cancel any previous request
    if (abortControllerRef.current) {
      abortControllerRef.current.abort();
    }

    abortControllerRef.current = new AbortController();
    setLoading(true);
    setError(null);

    try {
      const response = await fetch(url, {
        signal: abortControllerRef.current.signal,
      });

      if (!response.ok) {
        throw new Error(`HTTP error! Status: ${response.status}`);
      }

      const result = await response.json();

      setData(result);
      setLastUpdate(Date.now());
      setStatus('polling');
      retryCountRef.current = 0; // Reset retry on success

      if (onData) onData(result);
    } catch (err) {
      if (err.name === 'AbortError') return;

      console.error('Fetch error:', err);
      const message = err.message || 'Failed to fetch data';

      setError(message);
      setStatus('error');

      if (onError) onError(err);

      // Exponential backoff retry (only for automatic polling)
      if (!isManual && retryCountRef.current < backoffDelays.length) {
        const delay = backoffDelays[retryCountRef.current];
        retryCountRef.current += 1;

        setTimeout(() => {
          if (isMountedRef.current && !isPausedRef.current) {
            fetchData();
          }
        }, delay);
      }
    } finally {
      setLoading(false);
      abortControllerRef.current = null;
    }
  };

  const startPolling = () => {
    if (intervalRef.current) return;

    isPausedRef.current = false;
    setStatus('polling');

    // Initial fetch
    fetchData();

    // Then set interval
    intervalRef.current = setInterval(() => {
      if (!isPausedRef.current) {
        fetchData();
      }
    }, interval);
  };

  const pausePolling = () => {
    isPausedRef.current = true;
    setStatus('paused');

    if (intervalRef.current) {
      clearInterval(intervalRef.current);
      intervalRef.current = null;
    }
  };

  const resumePolling = () => {
    isPausedRef.current = false;
    setStatus('polling');
    startPolling(); // Will do initial fetch + set new interval
  };

  const manualRefresh = () => {
    fetchData(true); // Force fetch regardless of pause state
  };

  // Handle page visibility
  useEffect(() => {
    if (!pauseOnHidden) return;

    const handleVisibilityChange = () => {
      if (document.hidden) {
        pausePolling();
      } else if (status !== 'paused' && status !== 'error') {
        resumePolling();
      }
    };

    document.addEventListener('visibilitychange', handleVisibilityChange);

    return () => {
      document.removeEventListener('visibilitychange', handleVisibilityChange);
    };
  }, [pauseOnHidden, status]);

  // Main polling lifecycle
  useEffect(() => {
    isMountedRef.current = true;
    startPolling();

    return () => {
      isMountedRef.current = false;

      // Comprehensive cleanup
      if (intervalRef.current) {
        clearInterval(intervalRef.current);
        intervalRef.current = null;
      }

      if (abortControllerRef.current) {
        abortControllerRef.current.abort();
        abortControllerRef.current = null;
      }
    };
  }, [url, interval]); // Re-start polling if url or interval changes

  // Helpers
  const getTimeSinceUpdate = () => {
    if (!lastUpdate) return 'Never';
    const diff = Date.now() - lastUpdate;
    const seconds = Math.floor(diff / 1000);
    if (seconds < 60) return `${seconds}s ago`;
    const minutes = Math.floor(seconds / 60);
    if (minutes < 60) return `${minutes}m ago`;
    return `${Math.floor(minutes / 60)}h ago`;
  };

  const getStatusColor = () => {
    switch (status) {
      case 'polling':
        return '#28a745';
      case 'paused':
        return '#ffc107';
      case 'error':
        return '#dc3545';
      default:
        return '#6c757d';
    }
  };

  return (
    <div
      style={{
        padding: '20px',
        maxWidth: '800px',
        margin: '0 auto',
        fontFamily: 'system-ui, sans-serif',
      }}
    >
      <h2>Advanced Polling Component</h2>

      {/* Status bar */}
      <div
        style={{
          padding: '14px',
          marginBottom: '20px',
          backgroundColor: '#f8f9fa',
          border: `2px solid ${getStatusColor()}`,
          borderRadius: '8px',
          display: 'flex',
          justifyContent: 'space-between',
          alignItems: 'center',
        }}
      >
        <div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
          <div
            style={{
              width: '12px',
              height: '12px',
              borderRadius: '50%',
              backgroundColor: getStatusColor(),
            }}
          />
          <strong>Status:</strong>{' '}
          {status.charAt(0).toUpperCase() + status.slice(1)}
          {retryCountRef.current > 0 && status === 'error' && (
            <span style={{ marginLeft: '12px', color: '#dc3545' }}>
              Retry attempt {retryCountRef.current}
            </span>
          )}
        </div>
        <div style={{ fontSize: '14px', color: '#555' }}>
          Last update: {getTimeSinceUpdate()}
        </div>
      </div>

      {/* Controls */}
      <div style={{ display: 'flex', gap: '12px', marginBottom: '24px' }}>
        {status === 'polling' ? (
          <button
            onClick={pausePolling}
            style={{
              padding: '10px 20px',
              backgroundColor: '#ffc107',
              color: '#000',
              border: 'none',
              borderRadius: '6px',
              cursor: 'pointer',
              fontWeight: 600,
            }}
          >
            ⏸ Pause
          </button>
        ) : (
          <button
            onClick={resumePolling}
            style={{
              padding: '10px 20px',
              backgroundColor: '#28a745',
              color: 'white',
              border: 'none',
              borderRadius: '6px',
              cursor: 'pointer',
              fontWeight: 600,
            }}
          >
            ▶ Resume
          </button>
        )}

        <button
          onClick={manualRefresh}
          disabled={loading}
          style={{
            padding: '10px 20px',
            backgroundColor: '#007bff',
            color: 'white',
            border: 'none',
            borderRadius: '6px',
            cursor: loading ? 'not-allowed' : 'pointer',
            opacity: loading ? 0.6 : 1,
            fontWeight: 600,
          }}
        >
          {loading ? 'Fetching...' : '🔄 Refresh Now'}
        </button>
      </div>

      {/* Loading */}
      {loading && (
        <div
          style={{
            padding: '12px',
            backgroundColor: '#e3f2fd',
            border: '1px solid #2196f3',
            borderRadius: '6px',
            textAlign: 'center',
            marginBottom: '20px',
          }}
        >
          ⏳ Fetching latest data...
        </div>
      )}

      {/* Error */}
      {error && (
        <div
          style={{
            padding: '16px',
            backgroundColor: '#f8d7da',
            border: '1px solid #dc3545',
            borderRadius: '6px',
            marginBottom: '20px',
            color: '#721c24',
          }}
        >
          <strong>Error:</strong> {error}
          {retryCountRef.current > 0 && (
            <div style={{ marginTop: '8px', fontSize: '14px' }}>
              Will retry in{' '}
              {Math.round(backoffDelays[retryCountRef.current - 1] / 1000)}s...
            </div>
          )}
        </div>
      )}

      {/* Data display */}
      <div
        style={{
          padding: '20px',
          backgroundColor: 'white',
          border: '1px solid #dee2e6',
          borderRadius: '8px',
          minHeight: '220px',
        }}
      >
        <h3>Data</h3>
        {data ? (
          <pre
            style={{
              backgroundColor: '#f8f9fa',
              padding: '16px',
              borderRadius: '6px',
              overflow: 'auto',
              fontSize: '14px',
            }}
          >
            {JSON.stringify(data, null, 2)}
          </pre>
        ) : (
          <p style={{ color: '#777', textAlign: 'center', marginTop: '60px' }}>
            Waiting for first successful poll...
          </p>
        )}
      </div>
    </div>
  );
}

/*
Expected behavior:

• Mounts → starts polling immediately
• Data updates every `interval` ms (default 5000)
• Pause button → stops polling, status → paused
• Resume button → restarts polling + immediate fetch
• Refresh Now → forces fetch even when paused
• Tab hidden (if pauseOnHidden) → auto-pauses
• Tab visible again → auto-resumes (if was polling before)
• Network error → shows error + exponential backoff retries
• Request in progress → can be cancelled on new request / unmount
• Unmount → clears interval + aborts fetch → no memory leak
*/

📊 PHẦN 4: SO SÁNH PATTERNS (30 phút)

Bảng So Sánh: useState vs useRef

Tiêu chíuseStateuseRefKhi nào dùng?
Re-render✅ Trigger re-render❌ KHÔNG trigger re-renderuseState: UI data
useRef: Non-UI data
Update timingAsync (batched)Sync (immediate)useState: Cần consistency
useRef: Cần instant access
PurposeUI state managementMutable values persistenceuseState: Hiển thị lên UI
useRef: Internal tracking
Access patternvalueref.current-
Persistence✅ Persists across renders✅ Persists across rendersCả hai đều persist
Initial valueuseState(initial)useRef(initial)-
PerformanceCan cause re-rendersNo re-render overheaduseRef: Better cho high-frequency updates
Use casesForm inputs, toggles, counters hiển thịTimer IDs, previous values, flags-

Trade-offs Chi Tiết

✅ Khi nào PHẢI dùng useState:

jsx
// 1. UI data - cần hiển thị lên màn hình
const [count, setCount] = useState(0);
return <p>Count: {count}</p>; // ✅ UI needs this

// 2. Conditional rendering
const [isOpen, setIsOpen] = useState(false);
return isOpen ? <Modal /> : null; // ✅ Affects what renders

// 3. Derived values dùng trong JSX
const [items, setItems] = useState([]);
return <p>Total: {items.length}</p>; // ✅ UI depends on this

// 4. Props passed to children
const [theme, setTheme] = useState('dark');
return <Button theme={theme} />; // ✅ Child needs this

✅ Khi nào PHẢI dùng useRef:

jsx
// 1. Timer/Interval IDs
const intervalRef = useRef(null);
intervalRef.current = setInterval(/* ... */); // ✅ Non-UI, no need to render

// 2. Previous values
const prevCountRef = useRef(count);
useEffect(() => {
  prevCountRef.current = count;
}); // ✅ Tracking, not displaying

// 3. Flags không ảnh hưởng UI
const isMountedRef = useRef(true);
useEffect(() => () => {
  isMountedRef.current = false;
}); // ✅ Internal flag

// 4. Mutable values thay đổi thường xuyên
const renderCountRef = useRef(0);
renderCountRef.current += 1; // ✅ Would cause infinite loop with useState

Decision Tree

                    Cần lưu giá trị?

           ┌───────────────┴───────────────┐
           │                               │
      Có (persist)                    Không (local variable)

    Giá trị này hiển thị UI?

    ┌──────┴──────┐
    │             │
   Có           Không
    │             │
useState       useRef

    Ví dụ useState:      Ví dụ useRef:
    - Counter display    - Timer ID
    - Form values        - Previous value
    - Toggle state       - Render count
    - Loading state      - Abort controller
    - Error message      - Flag variables

Pattern Combinations

Pattern 1: useState + useRef cho Derived State

jsx
// ✅ GOOD: useState cho source, useRef cho derived
function SearchWithHistory() {
  const [searchTerm, setSearchTerm] = useState('');
  const previousSearchRef = useRef('');

  useEffect(() => {
    previousSearchRef.current = searchTerm;
  }, [searchTerm]);

  const searchChanged = searchTerm !== previousSearchRef.current;

  return (
    <div>
      <input
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
      />
      {searchChanged && <p>Search term changed!</p>}
    </div>
  );
}
jsx
// ✅ GOOD: Group related refs
function ComplexTimer() {
  const timerRefs = useRef({
    intervalId: null,
    startTime: null,
    pausedTime: null
  });

  const start = () => {
    timerRefs.current.startTime = Date.now();
    timerRefs.current.intervalId = setInterval(/* ... */);
  };

  const pause = () => {
    timerRefs.current.pausedTime = Date.now();
    clearInterval(timerRefs.current.intervalId);
  };

  return (/* ... */);
}

Pattern 3: Ref cho Optimization

jsx
// ✅ GOOD: useRef tránh unnecessary re-renders
function ChatRoom() {
  const [messages, setMessages] = useState([]);
  const scrollRef = useRef(null);
  const prevMessagesLengthRef = useRef(messages.length);

  useEffect(() => {
    // Chỉ scroll nếu có message mới
    if (messages.length > prevMessagesLengthRef.current) {
      scrollRef.current?.scrollIntoView({ behavior: 'smooth' });
    }
    prevMessagesLengthRef.current = messages.length;
  }, [messages]);

  return (
    <div>
      {messages.map((msg) => (
        <div key={msg.id}>{msg.text}</div>
      ))}
      <div ref={scrollRef} />
    </div>
  );
}

🧪 PHẦN 5: DEBUG LAB (20 phút)

Bug 1: Stale Ref Value ⭐

jsx
// ❌ BUG: Ref value không update như mong đợi
function BuggyCounter() {
  const [count, setCount] = useState(0);
  const countRef = useRef(count);

  const logCount = () => {
    console.log('Ref value:', countRef.current); // ⚠️ Luôn 0!
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount((c) => c + 1)}>Increment</button>
      <button onClick={logCount}>Log Ref</button>
    </div>
  );
}

🔍 Debug Questions:

  1. Tại sao countRef.current luôn là 0?
  2. Ref được update khi nào?
  3. Làm sao fix?

💡 Giải thích:

jsx
// ❌ VẤN ĐỀ:
const countRef = useRef(count);
// useRef chỉ chạy lần đầu (mount)
// count thay đổi nhưng ref KHÔNG tự động sync!

// ✅ SOLUTION 1: Manual sync với useEffect
function FixedCounter() {
  const [count, setCount] = useState(0);
  const countRef = useRef(count);

  useEffect(() => {
    countRef.current = count; // Sync manually
  }, [count]);

  const logCount = () => {
    console.log('Ref value:', countRef.current); // ✅ Correct!
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount((c) => c + 1)}>Increment</button>
      <button onClick={logCount}>Log Ref</button>
    </div>
  );
}

// ✅ SOLUTION 2: Đọc trực tiếp từ state (nếu có thể)
function BetterCounter() {
  const [count, setCount] = useState(0);

  const logCount = () => {
    console.log('Count value:', count); // ✅ Always correct
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount((c) => c + 1)}>Increment</button>
      <button onClick={logCount}>Log Count</button>
    </div>
  );
}

Bug 2: Memory Leak - Không Cleanup Timer ⭐⭐

jsx
// ❌ BUG: Memory leak khi component unmount
function BuggyTimer() {
  const [count, setCount] = useState(0);
  const intervalRef = useRef(null);

  const startTimer = () => {
    intervalRef.current = setInterval(() => {
      setCount((c) => c + 1);
    }, 1000);
  };

  const stopTimer = () => {
    clearInterval(intervalRef.current);
    intervalRef.current = null;
  };

  // ⚠️ THIẾU CLEANUP!

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={startTimer}>Start</button>
      <button onClick={stopTimer}>Stop</button>
    </div>
  );
}

🔍 Debug Questions:

  1. Điều gì xảy ra nếu component unmount khi timer đang chạy?
  2. Làm sao detect memory leak?
  3. Cách fix đúng?

💡 Giải thích:

jsx
// ❌ VẤN ĐỀ:
// Component unmount → interval vẫn chạy → call setCount → error + memory leak

// ⚠️ Error trong console:
// "Warning: Can't perform a React state update on an unmounted component"

// ✅ SOLUTION: useEffect cleanup
function FixedTimer() {
  const [count, setCount] = useState(0);
  const intervalRef = useRef(null);

  const startTimer = () => {
    if (intervalRef.current) return; // Prevent multiple intervals

    intervalRef.current = setInterval(() => {
      setCount((c) => c + 1);
    }, 1000);
  };

  const stopTimer = () => {
    if (intervalRef.current) {
      clearInterval(intervalRef.current);
      intervalRef.current = null;
    }
  };

  // ✅ Cleanup on unmount
  useEffect(() => {
    return () => {
      if (intervalRef.current) {
        clearInterval(intervalRef.current);
      }
    };
  }, []); // Empty deps = chỉ mount/unmount

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={startTimer}>Start</button>
      <button onClick={stopTimer}>Stop</button>
    </div>
  );
}

🔍 Cách test memory leak:

jsx
function App() {
  const [showTimer, setShowTimer] = useState(true);

  return (
    <div>
      <button onClick={() => setShowTimer(!showTimer)}>Toggle Timer</button>

      {showTimer && <FixedTimer />}
    </div>
  );
}

// Test steps:
// 1. Start timer
// 2. Click "Toggle Timer" (unmount component)
// 3. Check console - không có warning = good!
// 4. Open DevTools → Memory → Take heap snapshot
// 5. Unmount/remount nhiều lần
// 6. Take another snapshot
// 7. Compare → detached intervals = memory leak

Bug 3: Ref vs State Confusion ⭐⭐⭐

jsx
// ❌ BUG: Dùng ref cho UI data
function BuggyTodoList() {
  const todosRef = useRef([]);

  const addTodo = (text) => {
    todosRef.current = [...todosRef.current, { id: Date.now(), text }];
    console.log('Todos:', todosRef.current); // ✅ Updated in ref
  };

  return (
    <div>
      <button onClick={() => addTodo('New todo')}>Add Todo</button>

      <ul>
        {todosRef.current.map((todo) => (
          <li key={todo.id}>{todo.text}</li>
        ))}
      </ul>
      {/* ⚠️ UI never updates! */}
    </div>
  );
}

🔍 Debug Questions:

  1. Tại sao UI không update?
  2. Console.log shows correct data nhưng UI stale?
  3. Khi nào thì UI update?

💡 Giải thích:

jsx
// ❌ VẤN ĐỀ:
// - todosRef.current thay đổi ✅
// - Nhưng không trigger re-render ❌
// - UI chỉ render lần đầu với empty array

// ✅ SOLUTION: Dùng useState cho UI data
function FixedTodoList() {
  const [todos, setTodos] = useState([]);

  const addTodo = (text) => {
    setTodos((prev) => [...prev, { id: Date.now(), text }]); // ✅ Triggers re-render
  };

  return (
    <div>
      <button onClick={() => addTodo('New todo')}>Add Todo</button>

      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>{todo.text}</li>
        ))}
      </ul>
    </div>
  );
}

// 💡 QUY TẮC:
// If it's displayed in JSX → useState
// If it's internal tracking → useRef

Khi nào UI sẽ update nếu dùng ref?

jsx
function WhenRefUpdates() {
  const countRef = useRef(0);
  const [, forceRender] = useState({});

  const increment = () => {
    countRef.current += 1;
    // UI vẫn không update!
  };

  const incrementAndRender = () => {
    countRef.current += 1;
    forceRender({}); // ✅ Force re-render
    // Bây giờ UI mới update!
  };

  return (
    <div>
      <p>Count: {countRef.current}</p>
      <button onClick={increment}>Increment (no update)</button>
      <button onClick={incrementAndRender}>Increment (with update)</button>
    </div>
  );
}

// ⚠️ Nhưng đây là ANTI-PATTERN!
// Nếu cần UI update → dùng useState!

✅ PHẦN 6: TỰ ĐÁNH GIÁ (15 phút)

Knowledge Check

Đánh dấu các câu bạn có thể trả lời tự tin:

  • [ ] useRef trả về gì và object đó có cấu trúc như thế nào?
  • [ ] Tại sao update ref.current không trigger re-render?
  • [ ] Sự khác biệt chính giữa useState và useRef là gì?
  • [ ] Khi nào nên dùng useRef thay vì useState?
  • [ ] Làm sao track previous value của một state?
  • [ ] Tại sao cần cleanup timers trong useEffect?
  • [ ] useRef có thể thay thế useState để tối ưu performance không?
  • [ ] Ref object có thay đổi giữa các lần render không?
  • [ ] Có thể dùng useRef để store object/array không?
  • [ ] Làm sao debug memory leak từ timers?

Code Review Checklist

Khi review code có useRef, check:

✅ Correct Usage:

  • [ ] Dùng ref cho non-UI data (timer IDs, flags, previous values)
  • [ ] Dùng state cho UI data
  • [ ] Update ref.current synchronously khi cần
  • [ ] Không dựa vào ref.current để trigger re-renders

✅ Cleanup:

  • [ ] Tất cả timers đều được cleared
  • [ ] useEffect có return cleanup function
  • [ ] Cleanup chạy on unmount và trước next effect
  • [ ] Refs được reset khi cần (set về null)

✅ Edge Cases:

  • [ ] Handle multiple starts (prevent duplicate timers)
  • [ ] Handle unmount mid-operation
  • [ ] Validate ref.current trước khi dùng
  • [ ] Consider race conditions

✅ Performance:

  • [ ] Không setState unnecessarily
  • [ ] Refs được dùng đúng chỗ (không gây extra renders)
  • [ ] No memory leaks

❌ Common Mistakes:

  • [ ] Không dùng ref cho UI data
  • [ ] Không quên cleanup
  • [ ] Không expect ref thay đổi trigger render
  • [ ] Không dùng ref initial value như useState

🏠 BÀI TẬP VỀ NHÀ

Bắt buộc (30 phút)

Exercise: Custom useTimeout Hook

jsx
/**
 * 🎯 Mục tiêu: Tạo custom hook wrap setTimeout
 *
 * Requirements:
 * 1. Hook nhận callback và delay
 * 2. Tự động cleanup on unmount
 * 3. Có thể reset timer
 * 4. Có thể cancel timer
 *
 * API:
 * const { reset, cancel } = useTimeout(callback, delay);
 */

// TODO: Implement useTimeout
function useTimeout(callback, delay) {
  // Hints:
  // - useRef cho timeout ID
  // - useRef cho callback (để tránh stale closure)
  // - useEffect để setup/cleanup
  // - Return reset và cancel functions
}

// Usage example:
function NotificationDemo() {
  const [show, setShow] = useState(false);

  const { reset, cancel } = useTimeout(() => {
    setShow(false);
  }, 3000);

  const showNotification = () => {
    setShow(true);
    reset(); // Reset timer
  };

  return (
    <div>
      <button onClick={showNotification}>Show Notification</button>
      {show && (
        <div>
          <p>This will disappear in 3 seconds</p>
          <button onClick={cancel}>Keep it</button>
        </div>
      )}
    </div>
  );
}
💡 Solution
jsx
/**
 * Custom useTimeout Hook
 * Wraps setTimeout with proper cleanup and control methods
 * Uses useRef to store timeout ID and latest callback
 * @param {Function} callback - Function to execute after delay
 * @param {number} delay - Delay in milliseconds (null/undefined to not set timer)
 * @returns {{ reset: Function, cancel: Function }} - Control methods
 */
function useTimeout(callback, delay) {
  const timeoutRef = useRef(null);
  const callbackRef = useRef(callback);

  // Always keep the latest callback in ref (avoid stale closures)
  useEffect(() => {
    callbackRef.current = callback;
  }, [callback]);

  // Setup / cleanup timeout
  useEffect(() => {
    // If delay is null/undefined → don't set timer
    if (delay == null) {
      return;
    }

    // Clear any existing timeout
    if (timeoutRef.current) {
      clearTimeout(timeoutRef.current);
    }

    // Set new timeout using latest callback
    timeoutRef.current = setTimeout(() => {
      callbackRef.current();
      timeoutRef.current = null; // Clean up after execution
    }, delay);

    // Cleanup on unmount or when delay/callback changes
    return () => {
      if (timeoutRef.current) {
        clearTimeout(timeoutRef.current);
        timeoutRef.current = null;
      }
    };
  }, [delay]); // Re-run when delay changes

  const reset = () => {
    // Clear existing and set new one with current delay
    if (timeoutRef.current) {
      clearTimeout(timeoutRef.current);
    }

    if (delay != null) {
      timeoutRef.current = setTimeout(() => {
        callbackRef.current();
        timeoutRef.current = null;
      }, delay);
    }
  };

  const cancel = () => {
    if (timeoutRef.current) {
      clearTimeout(timeoutRef.current);
      timeoutRef.current = null;
    }
  };

  return { reset, cancel };
}

// ────────────────────────────────────────────────
// Example usage:
function NotificationDemo() {
  const [show, setShow] = useState(false);

  const { reset, cancel } = useTimeout(() => {
    setShow(false);
  }, 3000);

  const showNotification = () => {
    setShow(true);
    reset(); // (re)start the 3-second timer
  };

  return (
    <div style={{ padding: '20px' }}>
      <button
        onClick={showNotification}
        style={{ padding: '10px 20px', fontSize: '16px' }}
      >
        Show Notification
      </button>

      {show && (
        <div
          style={{
            marginTop: '16px',
            padding: '16px',
            backgroundColor: '#d4edda',
            border: '1px solid #c3e6cb',
            borderRadius: '6px',
            display: 'flex',
            justifyContent: 'space-between',
            alignItems: 'center',
          }}
        >
          <p style={{ margin: 0 }}>
            This notification will disappear in 3 seconds
          </p>
          <button
            onClick={() => {
              cancel();
              // Optional: keep it visible longer or forever
            }}
            style={{
              padding: '6px 12px',
              backgroundColor: '#fff',
              border: '1px solid #28a745',
              borderRadius: '4px',
              cursor: 'pointer',
            }}
          >
            Keep it
          </button>
        </div>
      )}
    </div>
  );
}

/*
Expected behavior:

• Click "Show Notification" → message appears + 3s countdown starts
• After 3 seconds → message disappears automatically
• Click "Show Notification" again before 3s → timer resets → another full 3s
• Click "Keep it" → timer cancelled → message stays visible
• Component unmount → timeout cleaned up (no memory leak)
*/

Nâng cao (60 phút)

Exercise: Request Deduplication

jsx
/**
 * 🎯 Mục tiêu: Prevent duplicate API requests
 *
 * Scenario:
 * User clicks "Load Data" nhiều lần nhanh.
 * Bạn chỉ muốn gọi API 1 lần, các request sau dùng lại kết quả.
 *
 * Requirements:
 * 1. Track in-flight requests bằng ref
 * 2. If request pending → return existing promise
 * 3. If request done → return cached result (for 5s)
 * 4. After 5s → allow new request
 *
 * Bonus:
 * - Support multiple URLs (cache by URL)
 * - Request cancellation
 * - Error handling with retry
 */

function useDeduplicatedFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  // TODO: Implement với useRef
  // - Track pending request
  // - Cache results
  // - Deduplicate logic

  const fetchData = async () => {
    // TODO: Your implementation
  };

  return { data, loading, error, fetchData };
}
💡 Solution
jsx
/**
 * useDeduplicatedFetch - Hook with request deduplication & short-term caching
 * Prevents duplicate in-flight requests for the same URL
 * Caches successful result for 5 seconds
 * Supports cancellation via AbortController
 * @param {string} url - The API endpoint to fetch
 * @returns {{
 *   data: any,
 *   loading: boolean,
 *   error: string | null,
 *   fetchData: () => Promise<any>
 * }}
 */
function useDeduplicatedFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  // Refs for deduplication & caching
  const pendingPromiseRef = useRef(null); // current in-flight promise
  const abortControllerRef = useRef(null); // for cancellation
  const cacheRef = useRef({
    data: null,
    timestamp: 0,
    url: null,
  });

  const CACHE_DURATION = 5000; // 5 seconds

  const fetchData = useCallback(async () => {
    // 1. Check cache first (fast path)
    const now = Date.now();
    if (
      cacheRef.current.url === url &&
      cacheRef.current.data !== null &&
      now - cacheRef.current.timestamp < CACHE_DURATION
    ) {
      setData(cacheRef.current.data);
      setError(null);
      setLoading(false);
      return cacheRef.current.data;
    }

    // 2. If there's already a pending request for this URL → reuse it
    if (pendingPromiseRef.current) {
      setLoading(true);
      try {
        const result = await pendingPromiseRef.current;
        return result;
      } catch (err) {
        throw err;
      } finally {
        setLoading(false);
      }
    }

    // 3. No cache & no pending request → start new fetch
    setLoading(true);
    setError(null);

    const controller = new AbortController();
    abortControllerRef.current = controller;
    const signal = controller.signal;

    const promise = (async () => {
      try {
        const response = await fetch(url, { signal });

        if (!response.ok) {
          throw new Error(`HTTP ${response.status}`);
        }

        const result = await response.json();

        // Update cache
        cacheRef.current = {
          data: result,
          timestamp: Date.now(),
          url,
        };

        setData(result);
        setError(null);
        return result;
      } catch (err) {
        if (err.name === 'AbortError') {
          throw err; // let caller handle abort if needed
        }

        const message = err.message || 'Failed to fetch';
        setError(message);
        throw err;
      } finally {
        setLoading(false);
        // Clean up refs
        if (pendingPromiseRef.current === promise) {
          pendingPromiseRef.current = null;
        }
        if (abortControllerRef.current === controller) {
          abortControllerRef.current = null;
        }
      }
    })();

    // Store the promise for deduplication
    pendingPromiseRef.current = promise;

    return promise;
  }, [url]);

  // Cleanup on unmount or url change
  useEffect(() => {
    return () => {
      if (abortControllerRef.current) {
        abortControllerRef.current.abort();
        abortControllerRef.current = null;
      }
      pendingPromiseRef.current = null;
    };
  }, [url]);

  return { data, loading, error, fetchData };
}

// ────────────────────────────────────────────────
// Example usage component
function DeduplicationDemo() {
  const { data, loading, error, fetchData } = useDeduplicatedFetch(
    'https://jsonplaceholder.typicode.com/posts/1',
  );

  const handleMultipleClicks = () => {
    // Simulate user clicking "Load" button 5 times quickly
    for (let i = 0; i < 5; i++) {
      setTimeout(() => {
        fetchData().catch(() => {}); // ignore errors for demo
      }, i * 80); // slightly staggered
    }
  };

  return (
    <div style={{ padding: '24px', maxWidth: '700px', margin: '0 auto' }}>
      <h2>Request Deduplication Demo</h2>

      <button
        onClick={handleMultipleClicks}
        disabled={loading}
        style={{
          padding: '12px 24px',
          fontSize: '16px',
          marginBottom: '20px',
          backgroundColor: loading ? '#6c757d' : '#007bff',
          color: 'white',
          border: 'none',
          borderRadius: '6px',
          cursor: loading ? 'not-allowed' : 'pointer',
        }}
      >
        {loading ? 'Loading...' : 'Load Data (click many times)'}
      </button>

      {loading && (
        <div
          style={{
            padding: '12px',
            backgroundColor: '#e3f2fd',
            borderRadius: '6px',
            marginBottom: '16px',
          }}
        >
          Loading (only one real request sent)...
        </div>
      )}

      {error && (
        <div
          style={{
            padding: '16px',
            backgroundColor: '#f8d7da',
            color: '#721c24',
            borderRadius: '6px',
            marginBottom: '16px',
          }}
        >
          Error: {error}
        </div>
      )}

      {data && (
        <div
          style={{
            backgroundColor: '#f8f9fa',
            padding: '20px',
            borderRadius: '8px',
            border: '1px solid #dee2e6',
          }}
        >
          <h3>Data (fetched once):</h3>
          <pre style={{ fontSize: '14px', overflow: 'auto' }}>
            {JSON.stringify(data, null, 2)}
          </pre>
          <p style={{ color: '#666', fontSize: '14px', marginTop: '16px' }}>
            Subsequent clicks within 5 seconds will use cached result (no
            network)
          </p>
        </div>
      )}
    </div>
  );
}

/*
Expected behavior:

• Click "Load Data" once → real network request → data shown
• Click many times quickly (within ~100ms) → only ONE real request sent
  → all calls receive the same promise → same result
• After 5+ seconds → cache expires → next click triggers new request
• Rapid clicks after first success → instantly return cached data
• Component unmount / url change → pending request aborted
• No race conditions between multiple overlapping calls
*/

📚 TÀI LIỆU THAM KHẢO

Bắt buộc đọc

  1. React Docs - useRef:https://react.dev/reference/react/useRef
  2. React Docs - Referencing Values with Refs:https://react.dev/learn/referencing-values-with-refs

Đọc thêm

  1. When to use Ref vs State:https://kentcdodds.com/blog/usememo-and-usecallback

  2. Avoiding Memory Leaks:https://felixgerschau.com/react-hooks-memory-leaks/

  3. Understanding Refs:https://daveceddia.com/useref-hook/


🔗 KẾT NỐI KIẾN THỨC

Kiến thức nền (cần biết từ trước)

  • Ngày 11-12: useState fundamentals
  • Ngày 16-20: useEffect và cleanup
  • Ngày 19-20: Data fetching patterns

Hướng tới (sẽ dùng ở)

  • Ngày 22: useRef cho DOM manipulation
  • Ngày 23: useLayoutEffect với refs
  • Ngày 24: Custom hooks với useRef
  • Ngày 25: Project - Real-time Dashboard (combine tất cả)

💡 SENIOR INSIGHTS

Cân Nhắc Production

1. Performance Optimization

jsx
// ✅ GOOD: Dùng ref tránh unnecessary re-renders
function ExpensiveComponent({ data }) {
  const previousDataRef = useRef(data);
  const hasChanged = !shallowEqual(data, previousDataRef.current);

  useEffect(() => {
    if (hasChanged) {
      // Heavy computation
      previousDataRef.current = data;
    }
  }, [data, hasChanged]);

  return (/* ... */);
}

2. Debugging Tips

jsx
// ✅ GOOD: Add debug info với useRef
function DebuggedComponent() {
  const renderCount = useRef(0);
  const prevPropsRef = useRef();

  useEffect(() => {
    renderCount.current += 1;

    if (import.meta.env.DEV) {
      console.log('Render #', renderCount.current);
      console.log('Prev props:', prevPropsRef.current);
      console.log('Current props:', props);
    }

    prevPropsRef.current = props;
  });

  return (/* ... */);
}

3. Resource Management

jsx
// ✅ GOOD: Centralized resource cleanup
function useResourceManager() {
  const resourcesRef = useRef({
    timers: [],
    listeners: [],
    requests: [],
  });

  const addTimer = (id) => {
    resourcesRef.current.timers.push(id);
  };

  const addListener = (element, event, handler) => {
    element.addEventListener(event, handler);
    resourcesRef.current.listeners.push({ element, event, handler });
  };

  useEffect(() => {
    return () => {
      // Cleanup all resources
      resourcesRef.current.timers.forEach(clearInterval);
      resourcesRef.current.listeners.forEach(({ element, event, handler }) => {
        element.removeEventListener(event, handler);
      });
      resourcesRef.current.requests.forEach((req) => req.abort());
    };
  }, []);

  return { addTimer, addListener };
}

Câu Hỏi Phỏng Vấn

Junior Level:

Q1: "useRef và useState khác nhau như thế nào?"

Expected answer:

  • useState trigger re-render khi update, useRef không
  • useState cho UI data, useRef cho non-UI tracking
  • useState async update, useRef sync update
  • Cả hai đều persist across renders

Q2: "Làm sao store timer ID trong React?"

Expected answer:

  • Dùng useRef để store
  • Ví dụ: const timerRef = useRef(null)
  • Update: timerRef.current = setInterval(...)
  • Cleanup: clearInterval(timerRef.current)

Mid Level:

Q3: "Giải thích use case của useRef ngoài DOM manipulation."

Expected answer:

  • Previous value tracking
  • Timer/interval IDs
  • Flags (isMounted, isPaused)
  • Mutable instance variables
  • Avoiding stale closures
  • Request cancellation (AbortController)

Q4: "Có thể dùng useRef thay useState để optimize performance không?"

Expected answer:

  • Không thể replace hoàn toàn
  • useRef phù hợp cho non-UI data
  • Nếu data hiển thị lên UI → phải dùng useState
  • useRef tránh unnecessary re-renders cho internal tracking
  • Trade-off: Lose automatic UI sync

Senior Level:

Q5: "Design một system để track và cleanup tất cả subscriptions trong một component."

Expected answer:

jsx
function useSubscriptionManager() {
  const subscriptionsRef = useRef([]);

  const subscribe = (cleanup) => {
    subscriptionsRef.current.push(cleanup);
    return () => {
      const index = subscriptionsRef.current.indexOf(cleanup);
      if (index > -1) {
        subscriptionsRef.current.splice(index, 1);
      }
    };
  };

  useEffect(() => {
    return () => {
      subscriptionsRef.current.forEach((cleanup) => cleanup());
    };
  }, []);

  return subscribe;
}

Q6: "Explain memory leak patterns với useRef và cách prevent."

Expected answer:

  • Timers không cleared
  • Event listeners không removed
  • Refs trỏ đến large objects
  • Solutions:
    • useEffect cleanup
    • WeakRef cho circular references
    • Null out refs sau khi dùng
    • Resource tracking systems

War Stories

Story 1: The Infinite Loop Mystery

jsx
// ❌ BUG thực tế trong production:
function ChatRoom() {
  const [messages, setMessages] = useState([]);
  const wsRef = useRef(null);

  useEffect(() => {
    wsRef.current = new WebSocket(url);

    wsRef.current.onmessage = (e) => {
      setMessages([...messages, e.data]); // ⚠️ STALE CLOSURE!
    };
  }, []); // Missing dependency!

  return (/* ... */);
}

// ✅ FIX:
function ChatRoom() {
  const [messages, setMessages] = useState([]);
  const wsRef = useRef(null);

  useEffect(() => {
    wsRef.current = new WebSocket(url);

    wsRef.current.onmessage = (e) => {
      setMessages(prev => [...prev, e.data]); // ✅ Functional update
    };

    return () => wsRef.current?.close();
  }, []); // Safe now

  return (/* ... */);
}

Lesson learned:

  • Luôn dùng functional updates trong callbacks lâu dài
  • Refs giúp tránh stale closures
  • useEffect dependencies phải chính xác

Story 2: The Memory Leak That Cost $1000

Real story: Dashboard component không cleanup intervals → memory tăng dần → server crash → AWS bill spike.

jsx
// ❌ Production bug:
function Dashboard() {
  useEffect(() => {
    const interval = setInterval(fetchData, 5000);
    // ⚠️ No cleanup!
  }, []);
}

// ✅ Fix:
function Dashboard() {
  const intervalRef = useRef(null);

  useEffect(() => {
    intervalRef.current = setInterval(fetchData, 5000);

    return () => {
      if (intervalRef.current) {
        clearInterval(intervalRef.current);
      }
    };
  }, []);
}

Lesson learned:

  • ALWAYS cleanup resources
  • Monitor memory usage in production
  • Test component unmount scenarios

🎯 PREVIEW NGÀY MAI

Ngày 22: useRef - DOM Manipulation 🎨

Ngày mai chúng ta sẽ học use case thứ 2 của useRef: accessing DOM nodes.

Bạn sẽ học:

  • Ref forwarding với DOM elements
  • Focus management
  • Scroll control
  • Measuring DOM nodes
  • Third-party library integration
  • When to use refs vs state for DOM

Chuẩn bị mental model:

useRef = {
  Use Case 1: Mutable values (hôm nay) ✅
  Use Case 2: DOM references (ngày mai) 🎯
}

See you tomorrow! 🚀


✅ CHECKLIST HOÀN THÀNH

Trước khi kết thúc ngày học, check:

  • [ ] Hiểu sâu useRef vs useState
  • [ ] Làm đủ 5 exercises
  • [ ] Đọc React docs về useRef
  • [ ] Làm bài tập về nhà
  • [ ] Review debug lab
  • [ ] Chuẩn bị cho ngày mai

🎉 Congratulations! Bạn đã hoàn thành Ngày 21!

Personal tech knowledge base