Skip to content

📅 NGÀY 16: useEffect - Introduction

📍 Phase 2, Tuần 4, Ngày 16 của 45

⏱️ Thời lượng: 3-4 giờ


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

  • [ ] Hiểu được khái niệm Side Effects trong React và tại sao chúng ta cần quản lý chúng
  • [ ] Sử dụng được useEffect hook với cú pháp cơ bản
  • [ ] Phân biệt được khi nào effect chạy với [] (empty deps) và không có deps
  • [ ] Áp dụng được useEffect cho các trường hợp thực tế: document.title, console.log, timers

🤔 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 sau để kích hoạt kiến thức nền:

  1. Câu 1: Khi bạn gọi setCount(count + 1), điều gì xảy ra với component?

    • Đáp án mong đợi: Component re-render
  2. Câu 2: Nếu bạn muốn chạy một đoạn code SAU KHI component đã hiển thị lên màn hình, bạn đặt nó ở đâu?

    • Đáp án: Hiện tại chưa biết cách! (Đây là lý do cần useEffect)
  3. Câu 3: useState giúp component "nhớ" data giữa các lần render. Vậy nếu bạn muốn "làm gì đó" mỗi khi component render thì sao?

    • Đáp án: Cũng chưa biết! (useEffect sẽ 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 tưởng tượng bạn đang xây dựng một Counter App đơn giản:

jsx
function Counter() {
  const [count, setCount] = useState(0);

  // ❌ PROBLEM: Bạn muốn update document title mỗi khi count thay đổi
  // Nhưng nếu làm thế này:
  document.title = `Count: ${count}`;

  return (
    <div>
      <h1>{count}</h1>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  );
}

Vấn đề với code trên:

  • Code document.title = ... chạy TRONG quá trình render
  • Render function phải PURE (không side effects)
  • Nếu render bị gọi nhiều lần (React 18 Strict Mode), title sẽ update nhiều lần không cần thiết
  • Khó kiểm soát TIMING (trước hay sau khi UI xuất hiện?)

Định nghĩa Side Effect:

Side Effect là BẤT KỲ thao tác nào mà:

  1. Ảnh hưởng đến thứ BÊN NGOÀI component
  2. KHÔNG liên quan trực tiếp đến việc render UI
  3. Cần chạy Ở MỘT THỜI ĐIỂM CỤ THỂ trong lifecycle

Ví dụ Side Effects phổ biến:

  • ✅ Fetch data từ API
  • ✅ Update document.title
  • ✅ Set timers (setTimeout, setInterval)
  • ✅ Subscribe to events (scroll, resize)
  • ✅ Manipulate DOM trực tiếp
  • ✅ Log analytics
  • ✅ Save to localStorage

1.2 Giải Pháp: useEffect Hook

useEffect là hook giúp bạn thực hiện side effects AN TOÀN trong function components.

Cú pháp cơ bản:

jsx
import { useEffect } from 'react';

function MyComponent() {
  useEffect(() => {
    // Code của bạn ở đây
    // Chạy SAU KHI component render
  });

  return <div>UI của bạn</div>;
}

GIẢI PHÁP cho Counter:

jsx
function Counter() {
  const [count, setCount] = useState(0);

  // ✅ ĐÚNG: Dùng useEffect
  useEffect(() => {
    document.title = `Count: ${count}`;
  });

  return (
    <div>
      <h1>{count}</h1>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  );
}

Tại sao cách này tốt hơn?

  1. Separation of Concerns: Render logic ≠ Side effect logic
  2. Predictable Timing: Effect chạy SAU khi DOM đã update
  3. React-controlled: React quyết định KHI NÀO chạy effect
  4. Cleanup support: Có thể dọn dẹp (sẽ học ở Ngày 18)

1.3 Mental Model: The Effect Timeline

┌─────────────────────────────────────────────────────────────┐
│                    COMPONENT LIFECYCLE                        │
└─────────────────────────────────────────────────────────────┘

1. React calls your component function

2. Component returns JSX

3. React updates the DOM

4. Browser paints the screen

5. useEffect runs ← EFFECT CHẠY Ở ĐÂY!

6. User sees updated UI + Effect đã chạy

═══════════════════════════════════════════════════════════════

QUAN TRỌNG:
- Effect chạy SAU khi user đã nhìn thấy UI
- Effect KHÔNG block browser paint
- Effect là ASYNCHRONOUS với rendering

Analogy dễ hiểu:

React như một nhà hàng:

  1. Bếp nấu món ăn (Render JSX)
  2. Phục vụ đem ra bàn (Update DOM)
  3. Khách ăn (User sees UI)
  4. Phục vụ quay lại hỏi "Món ăn có ngon không?" (useEffect runs)

useEffect là "hậu sự vụ" - làm sau khi món chính đã phục vụ xong!


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

❌ Hiểu lầm #1: "useEffect chạy TRƯỚC khi render"

jsx
function Wrong() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log('Effect'); // Chạy SAU
  });

  console.log('Render'); // Chạy TRƯỚC

  return <div>{count}</div>;
}

// Output:
// Render
// Effect  ← Chạy sau!

❌ Hiểu lầm #2: "useEffect chỉ chạy 1 lần"

jsx
function Wrong() {
  const [count, setCount] = useState(0);

  // Không có dependencies array → Chạy MỖI lần render!
  useEffect(() => {
    console.log('Effect runs');
  });

  return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

// Mỗi lần click → Re-render → Effect chạy lại!

❌ Hiểu lầm #3: "useEffect và event handler giống nhau"

jsx
// ❌ SAI: Dùng useEffect cho user interaction
function Wrong() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    // Effect chạy SAU render, không phải khi user click!
    console.log('Clicked?'); // SAI!
  });

  return <button onClick={() => setCount(count + 1)}>Click me</button>;
}

// ✅ ĐÚNG: Dùng event handler
function Correct() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount(count + 1);
    console.log('Clicked!'); // ĐÚNG!
  };

  return <button onClick={handleClick}>Click me</button>;
}

QUY TẮC VÀNG:

  • 🎯 useEffect: Cho side effects cần chạy SAU render (sync với state/props)
  • 🎯 Event handlers: Cho side effects từ USER ACTIONS (click, submit, etc.)

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

Demo 1: Pattern Cơ Bản - Document Title ⭐

jsx
/**
 * Demo: Update document title khi state thay đổi
 * Concepts: useEffect basic, no dependencies
 */

import { useState, useEffect } from 'react';

function DocumentTitleDemo() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('Guest');

  // Effect 1: Update title với count
  useEffect(() => {
    document.title = `Count: ${count}`;
    console.log('Effect 1 ran - Title updated');
  });

  // Effect 2: Log name changes
  useEffect(() => {
    console.log(`Effect 2 ran - Name is: ${name}`);
  });

  return (
    <div>
      <h2>Document Title Demo</h2>

      <div>
        <p>Count: {count}</p>
        <button onClick={() => setCount(count + 1)}>Increment</button>
      </div>

      <div>
        <input
          value={name}
          onChange={(e) => setName(e.target.value)}
          placeholder='Enter your name'
        />
      </div>

      <p>
        💡 Mở Console và thử: Click button HOẶC type vào input
        <br />
        ⚠️ Cả 2 effects đều chạy mỗi khi component re-render!
      </p>
    </div>
  );
}

Quan sát:

  1. Mỗi khi click button → count thay đổi → Component re-render → CẢ 2 effects chạy
  2. Mỗi khi nhập text → name thay đổi → Component re-render → CẢ 2 effects chạy
  3. ⚠️ VẤN ĐỀ: Effect 1 không cần chạy khi name thay đổi, và ngược lại!

💡 Solution Preview: Ngày 17 sẽ học Dependencies Array để giải quyết!


Demo 2: Kịch Bản Thực Tế - Logger Component ⭐⭐

jsx
/**
 * Demo: Component log mọi thay đổi state
 * Use case: Debugging, Analytics tracking
 */

import { useState, useEffect } from 'react';

function UserProfileLogger() {
  const [user, setUser] = useState({
    name: '',
    email: '',
    age: 0,
  });

  // Effect: Log mỗi khi user object thay đổi
  useEffect(() => {
    console.log('📊 User Profile Updated:', {
      timestamp: new Date().toISOString(),
      data: user,
    });

    // Giả lập gửi analytics
    // trackEvent('profile_updated', user);
  });

  const updateName = (e) => {
    setUser({ ...user, name: e.target.value });
  };

  const updateEmail = (e) => {
    setUser({ ...user, email: e.target.value });
  };

  const updateAge = (e) => {
    setUser({ ...user, age: parseInt(e.target.value) || 0 });
  };

  return (
    <div>
      <h2>User Profile Logger</h2>

      <div>
        <input
          type='text'
          placeholder='Name'
          value={user.name}
          onChange={updateName}
        />
      </div>

      <div>
        <input
          type='email'
          placeholder='Email'
          value={user.email}
          onChange={updateEmail}
        />
      </div>

      <div>
        <input
          type='number'
          placeholder='Age'
          value={user.age}
          onChange={updateAge}
        />
      </div>

      <div>
        <h3>Current Profile:</h3>
        <pre>{JSON.stringify(user, null, 2)}</pre>
      </div>

      <p>
        💡 Mở Console: Mỗi lần nhập → Effect log ra
        <br />
        🎯 Use case: Debugging state changes, Analytics
      </p>
    </div>
  );
}

Ứng dụng thực tế:

  • 📊 Analytics: Track user behavior
  • 🐛 Debugging: Log state changes để debug
  • 💾 Auto-save: Trigger save mỗi khi data thay đổi (sẽ học cách optimize ở Ngày 17)

Demo 3: Edge Cases - Multiple Effects ⭐⭐⭐

jsx
/**
 * Demo: Nhiều effects trong 1 component
 * Edge case: Effect order, effect interactions
 */

import { useState, useEffect } from 'react';

function MultipleEffectsDemo() {
  const [count, setCount] = useState(0);
  const [isEven, setIsEven] = useState(true);

  // Effect 1: Chạy TRƯỚC
  useEffect(() => {
    console.log('1️⃣ Effect 1: Count changed to', count);
  });

  // Effect 2: Chạy SAU Effect 1
  useEffect(() => {
    console.log('2️⃣ Effect 2: Checking if even...');
    setIsEven(count % 2 === 0);
  });

  // Effect 3: Chạy SAU Effect 2
  useEffect(() => {
    console.log('3️⃣ Effect 3: isEven is', isEven);
  });

  return (
    <div>
      <h2>Multiple Effects Demo</h2>

      <p>Count: {count}</p>
      <p>Is Even? {isEven ? 'Yes ✅' : 'No ❌'}</p>

      <button onClick={() => setCount(count + 1)}>Increment</button>

      <div>
        <h3>⚠️ QUAN TRỌNG:</h3>
        <ul>
          <li>Effects chạy theo THỨ TỰ khai báo (top to bottom)</li>
          <li>Effect 2 gọi setIsEven → Trigger RE-RENDER mới!</li>
          <li>Sau re-render mới → Tất cả effects chạy LẠI!</li>
        </ul>
      </div>
    </div>
  );
}

Khi click button, console output:

1️⃣ Effect 1: Count changed to 1
2️⃣ Effect 2: Checking if even...
3️⃣ Effect 3: isEven is true
// → setIsEven(false) trigger re-render →
1️⃣ Effect 1: Count changed to 1
2️⃣ Effect 2: Checking if even...
3️⃣ Effect 3: isEven is false

⚠️ LƯU Ý:

  • Calling setState trong effect → Trigger thêm render!
  • Có thể gây infinite loop nếu không cẩn thận
  • Ngày 17 sẽ học cách control với Dependencies Array

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

⭐ Level 1: Áp Dụng Concept (15 phút)

jsx
/**
 * 🎯 Mục tiêu: Sử dụng useEffect để log component lifecycle
 * ⏱️ Thời gian: 15 phút
 * 🚫 KHÔNG dùng: Dependencies array (chưa học), useRef, useLayoutEffect
 *
 * Requirements:
 * 1. Tạo component với 1 state counter
 * 2. Dùng useEffect để console.log "Component rendered" mỗi lần render
 * 3. Log ra giá trị hiện tại của counter
 * 4. Thêm button để increment counter
 *
 * 💡 Gợi ý: useEffect chạy SAU mỗi render
 */

// ❌ Cách SAI (Anti-pattern):
function WrongLifecycleLogger() {
  const [count, setCount] = useState(0);

  // SAI: Log trực tiếp trong render
  console.log('Component rendered'); // Chạy TRONG render, không phải SAU!

  return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

// Tại sao sai?
// - Log chạy TRONG quá trình render
// - Nếu render bị gọi nhiều lần (React Strict Mode), log spam
// - Không đúng timing (cần log SAU khi UI xuất hiện)

// ✅ Cách ĐÚNG (Best practice):
function CorrectLifecycleLogger() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log('✅ Component rendered with count:', count);
    // Chạy SAU khi DOM đã update
    // React kiểm soát timing
  });

  return <button onClick={() => setCount(count + 1)}>Count: {count}</button>;
}

// Tại sao tốt hơn?
// ✅ Effect chạy SAU render → Đúng timing
// ✅ React kiểm soát → Predictable
// ✅ Tách biệt render logic và side effect logic

// 🎯 NHIỆM VỤ CỦA BẠN:
function LifecycleLogger() {
  // TODO: Khai báo state counter với giá trị ban đầu 0

  // TODO: Dùng useEffect để log "Component rendered" + giá trị counter

  return (
    <div>
      <h2>Lifecycle Logger</h2>
      {/* TODO: Hiển thị counter */}
      {/* TODO: Button để increment */}
      <p>💡 Mở Console để xem log</p>
    </div>
  );
}

// ✅ Expected behavior:
// - Mỗi lần click → Counter tăng → Component re-render → Effect log ra
// - Console output: "Component rendered with count: 0", "... count: 1", etc.
💡 Solution
jsx
/**
 * LifecycleLogger - Level 1 Exercise
 * Mục tiêu: Sử dụng useEffect để log sau mỗi lần render
 * Yêu cầu: Log "Component rendered" cùng giá trị count hiện tại
 * Không dùng dependencies array
 */
function LifecycleLogger() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log(`Component rendered with count: ${count}`);
  });

  return (
    <div>
      <h2>Lifecycle Logger</h2>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <p>💡 Mở Console để xem log</p>
    </div>
  );
}

/*
Kết quả ví dụ trong console khi tương tác:

Component rendered with count: 0    ← mount lần đầu
Component rendered with count: 1    ← click lần 1
Component rendered with count: 2    ← click lần 2
Component rendered with count: 3    ← click lần 3
...
*/

⭐⭐ Level 2: Nhận Biết Pattern (25 phút)

jsx
/**
 * 🎯 Mục tiêu: Biết khi nào NÊN và KHÔNG NÊN dùng useEffect
 * ⏱️ Thời gian: 25 phút
 *
 * Scenario: Bạn có form đăng nhập với username và password.
 * Yêu cầu: Hiển thị message "Welcome [username]!" khi user nhập xong.
 *
 * 🤔 PHÂN TÍCH:
 *
 * Approach A: Dùng useEffect để tạo welcome message
 * Pros:
 * - Effect chạy sau render, đảm bảo username đã update
 * - Có thể log/track khi username thay đổi
 * Cons:
 * - Không cần thiết! Welcome message có thể tính TRỰC TIẾP từ username
 * - Tạo thêm render cycle (effect → setState → re-render)
 * - Over-engineering
 *
 * Approach B: Tính welcome message trực tiếp (Derived State)
 * Pros:
 * - Đơn giản, dễ hiểu
 * - Không tạo thêm render
 * - Performance tốt hơn
 * Cons:
 * - Không log được khi username thay đổi (nhưng có thể log trong onChange)
 *
 * 💭 BẠN CHỌN GÌ VÀ TẠI SAO?
 */

// ❌ Approach A: Dùng useEffect (KHÔNG CẦN THIẾT!)
function LoginFormWithEffect() {
  const [username, setUsername] = useState('');
  const [welcomeMsg, setWelcomeMsg] = useState('');

  // ❌ Không cần effect cho derived state!
  useEffect(() => {
    setWelcomeMsg(username ? `Welcome ${username}!` : '');
  });

  return (
    <div>
      <input
        value={username}
        onChange={(e) => setUsername(e.target.value)}
        placeholder='Username'
      />
      <p>{welcomeMsg}</p>
    </div>
  );
}

// ✅ Approach B: Derived State (TỐT HƠN!)
function LoginFormDerived() {
  const [username, setUsername] = useState('');

  // ✅ Tính trực tiếp, không cần effect
  const welcomeMsg = username ? `Welcome ${username}!` : '';

  return (
    <div>
      <input
        value={username}
        onChange={(e) => setUsername(e.target.value)}
        placeholder='Username'
      />
      <p>{welcomeMsg}</p>
    </div>
  );
}

// 🎯 NHIỆM VỤ CỦA BẠN:
// 1. Implement CẢ HAI approaches trên
// 2. Thêm console.log để đếm số lần render
// 3. So sánh hiệu suất (count renders)
// 4. Viết comment giải thích approach nào tốt hơn và tại sao
// 5. Đưa ra 2 ví dụ KHI NÀO nên dùng useEffect cho tương tự use case

// 💡 Gợi ý câu 5:
// - Khi nào thì PHẢI dùng effect thay vì derived state?
// - Hint: Nghĩ về side effects thực sự (document.title, localStorage, API calls)
💡 Solution
jsx
/**
 * Level 2: Nhận Biết Pattern - Final Answer
 * Approach B (Derived State) là lựa chọn đúng
 * useEffect KHÔNG BAO GIỜ được dùng cho việc tính toán giá trị hiển thị từ state hiện tại
 */

import { useState, useEffect } from 'react';

function LoginFormWithEffect() {
  const [username, setUsername] = useState('');
  const [welcomeMsg, setWelcomeMsg] = useState('');

  useEffect(() => {
    setWelcomeMsg(username ? `Welcome ${username}!` : '');
  }); // ← Sai hoàn toàn! Tạo re-render thừa

  return (
    <div>
      <input
        value={username}
        onChange={(e) => setUsername(e.target.value)}
      />
      <p>{welcomeMsg}</p>
    </div>
  );
}

function LoginFormDerived() {
  const [username, setUsername] = useState('');
  const welcomeMsg = username ? `Welcome ${username}!` : ''; // ← Đúng cách!

  return (
    <div>
      <input
        value={username}
        onChange={(e) => setUsername(e.target.value)}
      />
      <p>{welcomeMsg}</p>
    </div>
  );
}

/*
Kết quả khi gõ "Grok":
- Approach A: 3 re-renders (1 ban đầu + 1 từ setUsername + 1 từ setWelcomeMsg)
- Approach B: Chỉ 2 re-renders (1 ban đầu + 1 từ setUsername)

Quy tắc vàng:
- Nếu giá trị chỉ dùng để render → Derived state
- Nếu ảnh hưởng bên ngoài (title, localStorage, API, analytics...) → useEffect
*/

⭐⭐⭐ Level 3: Kịch Bản Thực Tế (40 phút)

jsx
/**
 * 🎯 Mục tiêu: Xây dựng Page Title Manager
 * ⏱️ Thời gian: 40 phút
 *
 * 📋 Product Requirements:
 * User Story: "Là người dùng, tôi muốn thấy browser tab title
 * phản ánh đúng trang tôi đang xem và số notifications chưa đọc"
 *
 * ✅ Acceptance Criteria:
 * - [ ] Default title: "My App"
 * - [ ] Khi có notifications: "My App (3)" - số trong ngoặc
 * - [ ] Khi user ở trang khác: "[Page Name] - My App (3)"
 * - [ ] Title update ngay lập tức khi:
 *   - Notifications count thay đổi
 *   - Page name thay đổi
 *
 * 🎨 Technical Constraints:
 * - Chỉ dùng useState và useEffect (đã học đến Ngày 16)
 * - KHÔNG dùng dependencies array (chưa học)
 * - KHÔNG dùng useRef (chưa học)
 *
 * 🚨 Edge Cases cần handle:
 * - Notifications = 0 → Không hiển thị "(0)"
 * - Page name = 'Home' → Chỉ hiển thị "My App"
 * - Notifications > 99 → Hiển thị "(99+)"
 *
 * 📝 Implementation Checklist:
 * - [ ] State cho notifications count
 * - [ ] State cho current page name
 * - [ ] useEffect để sync document.title
 * - [ ] Buttons để test: increment/decrement notifications
 * - [ ] Buttons để switch pages
 * - [ ] Display current title trong UI
 */

// 🎯 STARTER CODE:
function PageTitleManager() {
  const [notifications, setNotifications] = useState(0);
  const [currentPage, setCurrentPage] = useState('Home');

  // TODO: useEffect để update document.title
  // Logic:
  // 1. Nếu currentPage === 'Home' → "My App"
  // 2. Nếu khác → "[Page Name] - My App"
  // 3. Nếu notifications > 0 → thêm " (count)" hoặc " (99+)"

  const incrementNotifications = () => {
    // TODO: Implement
  };

  const decrementNotifications = () => {
    // TODO: Implement (không cho âm)
  };

  const goToPage = (pageName) => {
    // TODO: Implement
  };

  return (
    <div>
      <h2>Page Title Manager</h2>

      {/* Display current title */}
      <div>
        <strong>Current Title:</strong>
        <code>{/* TODO: Lấy document.title và hiển thị */}</code>
      </div>

      {/* Notifications controls */}
      <div>
        <h3>Notifications: {notifications}</h3>
        {/* TODO: Buttons +1, -1, Reset */}
      </div>

      {/* Page navigation */}
      <div>
        <h3>Current Page: {currentPage}</h3>
        {/* TODO: Buttons cho Home, Profile, Settings, Messages */}
      </div>

      <div>
        <h3>Test Cases:</h3>
        <ul>
          <li>✅ Home + 0 notifs → "My App"</li>
          <li>✅ Home + 3 notifs → "My App (3)"</li>
          <li>✅ Profile + 5 notifs → "Profile - My App (5)"</li>
          <li>✅ Messages + 100 notifs → "Messages - My App (99+)"</li>
        </ul>
      </div>
    </div>
  );
}

// 💡 HINTS:
// - Dùng template literals: `${page} - My App ${notifString}`
// - Tách logic tính title thành helper function
// - Test bằng cách mở tab browser và xem title thay đổi
💡 Solution
jsx
/**
 * PageTitleManager - Level 3 Exercise
 * Yêu cầu: Quản lý document.title dựa trên trang hiện tại và số thông báo
 * Constraints: Chỉ dùng useState + useEffect (không dùng deps array)
 */
function PageTitleManager() {
  const [notifications, setNotifications] = useState(0);
  const [currentPage, setCurrentPage] = useState('Home');

  const getTitle = () => {
    let pagePart = currentPage === 'Home' ? '' : `${currentPage} - `;
    let notifPart = '';

    if (notifications > 0) {
      const displayCount = notifications > 99 ? '99+' : notifications;
      notifPart = `(${displayCount})`;
    }

    return `My App${pagePart ? ' ' + pagePart : ''}${notifPart}`.trim();
  };

  useEffect(() => {
    document.title = getTitle();
  });

  const incrementNotifications = () => setNotifications((n) => n + 1);
  const decrementNotifications = () =>
    setNotifications((n) => Math.max(0, n - 1));
  const resetNotifications = () => setNotifications(0);
  const goToPage = (pageName) => setCurrentPage(pageName);

  return (
    <div>
      <h2>Page Title Manager</h2>

      <div>
        <strong>Current Title:</strong> <code>{document.title}</code>
      </div>

      <div style={{ margin: '20px 0' }}>
        <h3>Notifications: {notifications}</h3>
        <button onClick={incrementNotifications}>+1</button>
        <button
          onClick={decrementNotifications}
          disabled={notifications === 0}
        >
          -1
        </button>
        <button onClick={resetNotifications}>Reset</button>
      </div>

      <div style={{ margin: '20px 0' }}>
        <h3>Current Page: {currentPage}</h3>
        <button onClick={() => goToPage('Home')}>Home</button>
        <button onClick={() => goToPage('Profile')}>Profile</button>
        <button onClick={() => goToPage('Settings')}>Settings</button>
        <button onClick={() => goToPage('Messages')}>Messages</button>
      </div>

      <div>
        <h3>Test Cases:</h3>
        <ul>
          <li>Home + 0 notifs → "My App"</li>
          <li>Home + 3 notifs → "My App (3)"</li>
          <li>Profile + 5 notifs → "Profile - My App (5)"</li>
          <li>Messages + 100 notifs → "Messages - My App (99+)"</li>
        </ul>
      </div>
    </div>
  );
}

/*
Kết quả ví dụ khi tương tác:

Mount ban đầu:
→ document.title = "My App"
→ UI hiển thị: Current Title: "My App"

Click +1 ba lần:
→ document.title = "My App (3)"

Click "Profile":
→ document.title = "Profile - My App (3)"

Click +1 đến 102:
→ document.title = "Profile - My App (99+)"

Click "Home" + Reset:
→ document.title = "My App"
*/

⭐⭐⭐⭐ Level 4: Quyết Định Kiến Trúc (60 phút)

jsx
/**
 * 🎯 Mục tiêu: Thiết kế Timer Component với nhiều features
 * ⏱️ Thời gian: 60 phút
 *
 * 🏗️ PHASE 1: Research & Design (20 phút)
 *
 * Context:
 * Bạn cần xây dựng Timer component hiển thị thời gian đã trôi qua
 * kể từ khi component mount. Timer cần:
 * - Tự động tick mỗi giây
 * - Có thể pause/resume
 * - Reset về 0
 * - Hiển thị format MM:SS
 *
 * Có 3 approaches khác nhau:
 *
 * APPROACH 1: setInterval trong useEffect (no dependencies)
 * Pros:
 * - Đơn giản, dễ hiểu
 * - setInterval chạy tự động
 * Cons:
 * - Effect chạy MỖI render → Tạo nhiều intervals!
 * - Memory leak (không clear interval)
 * - Không kiểm soát được khi nào create interval
 *
 * APPROACH 2: setTimeout đệ quy trong useEffect
 * Pros:
 * - Chính xác hơn setInterval
 * - Dễ cleanup
 * Cons:
 * - Phức tạp hơn
 * - Vẫn có vấn đề re-render
 *
 * APPROACH 3: setInterval + flag để prevent duplicate
 * Pros:
 * - Chỉ tạo 1 interval duy nhất
 * - Có kiểm soát
 * Cons:
 * - Cần state phụ để track (intervalId)
 * - Vẫn chưa cleanup (sẽ học ở Ngày 18)
 *
 * 💭 NHIỆM VỤ PHASE 1:
 * 1. Implement cả 3 approaches (prototype nhanh)
 * 2. Test và quan sát behavior (mở Console)
 * 3. Viết ADR (Architecture Decision Record)
 *
 * ADR Template:
 * ---
 * # ADR: Timer Implementation Strategy
 *
 * ## Context
 * [Mô tả vấn đề: Cần timer auto-tick, có pause/resume/reset]
 *
 * ## Decision
 * [Approach đã chọn: Approach 3]
 *
 * ## Rationale
 * [Tại sao chọn Approach 3:
 *  - Tránh được duplicate intervals
 *  - Có kiểm soát lifecycle
 *  - Trade-off: Phức tạp hơn nhưng ổn định hơn]
 *
 * ## Consequences
 * [Trade-offs accepted:
 *  - Cần state phụ (intervalId)
 *  - Code dài hơn
 *  - Vẫn cần cleanup (TODO: Ngày 18)]
 *
 * ## Alternatives Considered
 * [Approach 1, 2 và tại sao không chọn]
 * ---
 */

// 💻 PHASE 2: Implementation (30 phút)

// ❌ APPROACH 1: setInterval no control (BROKEN)
function TimerApproach1() {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    // ⚠️ VẤN ĐỀ: Effect chạy MỖI render
    // → Mỗi lần seconds update → Re-render → Effect chạy lại → TẠO INTERVAL MỚI!
    const id = setInterval(() => {
      setSeconds((s) => s + 1);
    }, 1000);

    // ❌ Không return cleanup → intervals chồng chéo!

    console.log('Created interval:', id);
  });

  return <div>Seconds: {seconds}</div>;
}

// ❌ APPROACH 2: setTimeout recursive (BETTER nhưng vẫn có issue)
function TimerApproach2() {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    // Vẫn chạy mỗi render!
    const id = setTimeout(() => {
      setSeconds((s) => s + 1);
    }, 1000);

    console.log('Created timeout:', id);

    // Cleanup tốt hơn nhưng vẫn tạo nhiều timeouts
  });

  return <div>Seconds: {seconds}</div>;
}

// ✅ APPROACH 3: Controlled với flag (BEST cho Ngày 16)
function TimerApproach3() {
  const [seconds, setSeconds] = useState(0);
  const [isRunning, setIsRunning] = useState(true);
  const [intervalId, setIntervalId] = useState(null);

  useEffect(() => {
    // Chỉ tạo interval nếu chưa có VÀ đang running
    if (isRunning && !intervalId) {
      const id = setInterval(() => {
        setSeconds((s) => s + 1);
      }, 1000);

      setIntervalId(id);
      console.log('✅ Created interval:', id);
    }

    // Nếu pause, clear interval
    if (!isRunning && intervalId) {
      clearInterval(intervalId);
      setIntervalId(null);
      console.log('⏸️ Cleared interval:', intervalId);
    }
  });

  const toggle = () => setIsRunning(!isRunning);

  const reset = () => {
    if (intervalId) clearInterval(intervalId);
    setIntervalId(null);
    setSeconds(0);
    setIsRunning(true);
  };

  // Format MM:SS
  const formatTime = (totalSeconds) => {
    const mins = Math.floor(totalSeconds / 60);
    const secs = totalSeconds % 60;
    return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
  };

  return (
    <div>
      <h2>Timer: {formatTime(seconds)}</h2>
      <button onClick={toggle}>{isRunning ? '⏸️ Pause' : '▶️ Resume'}</button>
      <button onClick={reset}>🔄 Reset</button>

      <p>Status: {isRunning ? 'Running' : 'Paused'}</p>
      <p>Interval ID: {intervalId || 'None'}</p>
    </div>
  );
}

// 🎯 NHIỆM VỤ CỦA BẠN:
// 1. Implement cả 3 approaches
// 2. Test từng approach, mở Console để xem intervals/timeouts được tạo
// 3. Viết ADR document chọn approach tốt nhất
// 4. Extend Approach 3 thêm:
//    - Lap counter (đếm vòng)
//    - Max time alert (cảnh báo khi > 5 phút)

// 🧪 PHASE 3: Testing (10 phút)
// Manual testing checklist:
// - [ ] Timer ticks đều đặn mỗi giây
// - [ ] Pause → Timer dừng
// - [ ] Resume → Timer tiếp tục từ giá trị cũ
// - [ ] Reset → Timer về 0 và chạy lại
// - [ ] Console không spam "Created interval"
// - [ ] Format MM:SS đúng (00:00, 01:23, 10:59, etc.)
💡 Solution
jsx
/**
 * TimerApproach3 - Level 4 Exercise (Best approach for Day 16)
 * Features: auto-tick every second, pause/resume, reset
 * Uses controlled interval with state flag to prevent duplicates
 * Format: MM:SS
 */
function TimerApproach3() {
  const [seconds, setSeconds] = useState(0);
  const [isRunning, setIsRunning] = useState(true);
  const [intervalId, setIntervalId] = useState(null);

  useEffect(() => {
    if (isRunning && !intervalId) {
      const id = setInterval(() => {
        setSeconds((s) => s + 1);
      }, 1000);

      setIntervalId(id);
      console.log('Created interval:', id);
    }

    if (!isRunning && intervalId) {
      clearInterval(intervalId);
      setIntervalId(null);
      console.log('Cleared interval:', intervalId);
    }

    // Note: Cleanup sẽ được học ở Ngày 18
    // return () => { if (intervalId) clearInterval(intervalId); };
  });

  const toggle = () => setIsRunning(!isRunning);

  const reset = () => {
    if (intervalId) {
      clearInterval(intervalId);
      setIntervalId(null);
    }
    setSeconds(0);
    setIsRunning(true);
  };

  const formatTime = (totalSeconds) => {
    const mins = Math.floor(totalSeconds / 60);
    const secs = totalSeconds % 60;
    return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
  };

  return (
    <div>
      <h2>Timer: {formatTime(seconds)}</h2>
      <button onClick={toggle}>{isRunning ? '⏸️ Pause' : '▶️ Resume'}</button>
      <button onClick={reset}>🔄 Reset</button>

      <p>Status: {isRunning ? 'Running' : 'Paused'}</p>
      <p>Interval ID: {intervalId || 'None'}</p>

      <p style={{ fontSize: '0.9em', color: '#666', marginTop: '2rem' }}>
        Approach được chọn: Approach 3 (Controlled interval với flag)
        <br />
        Lý do: Tránh duplicate intervals, dễ kiểm soát, ổn định nhất trong giới
        hạn Day 16
      </p>
    </div>
  );
}

/*
Kết quả ví dụ khi tương tác:

Mount ban đầu:
→ Timer: 00:00
→ Status: Running
→ Console: Created interval: [some number]

Sau 5 giây:
→ Timer: 00:05

Click Pause:
→ Timer: 00:05 (dừng)
→ Status: Paused
→ Console: Cleared interval: [number]

Click Resume:
→ Tiếp tục từ 00:05 → 00:06, 00:07...
→ Console: Created interval: [new number]

Click Reset:
→ Timer: 00:00
→ Status: Running
→ Console: Cleared interval: [old number]
     Created interval: [new number]
*/

⭐⭐⭐⭐⭐ Level 5: Production Challenge (90 phút)

jsx
/**
 * 🎯 Mục tiêu: Activity Tracker Dashboard
 * ⏱️ Thời gian: 90 phút
 *
 * 📋 Feature Specification:
 * Xây dựng dashboard track user activity:
 * 1. Time on page (thời gian ở trên trang)
 * 2. Mouse movements counter
 * 3. Keyboard presses counter
 * 4. Scroll depth tracker
 * 5. Tab visibility tracker (active/inactive)
 * 6. Activity summary (active/idle)
 *
 * 🏗️ Technical Design Doc:
 *
 * 1. Component Architecture:
 *    - ActivityDashboard (parent)
 *    - TimeTracker (time on page)
 *    - MouseTracker (movement count)
 *    - KeyboardTracker (keypress count)
 *    - ScrollTracker (scroll %)
 *    - VisibilityTracker (tab active/hidden)
 *    - ActivitySummary (overall stats)
 *
 * 2. State Management Strategy:
 *    - Mỗi tracker có state riêng
 *    - Parent aggregate tất cả stats
 *    - Lift state up pattern
 *
 * 3. Side Effects (useEffect usage):
 *    - setInterval cho time counter
 *    - Event listeners: mousemove, keydown, scroll, visibilitychange
 *    - ⚠️ Cleanup để prevent memory leaks (sẽ học ở Ngày 18, nhưng cần mention)
 *
 * 4. Performance Considerations:
 *    - Throttle mousemove events (mỗi 100ms mới update)
 *    - Debounce scroll tracking
 *    - Avoid re-rendering toàn bộ dashboard mỗi event
 *
 * 5. Error Handling Strategy:
 *    - Graceful degradation nếu browser không support API
 *    - Fallback values
 *
 * ✅ Production Checklist:
 * - [ ] State cho mỗi metric
 * - [ ] useEffect cho timers và event listeners
 * - [ ] Event handlers với throttle/debounce
 * - [ ] Conditional rendering cho empty states
 * - [ ] Accessibility: keyboard navigation, ARIA labels
 * - [ ] Display metrics trong clear, readable format
 * - [ ] Reset button để clear tất cả metrics
 * - [ ] Visual indicators (icons, colors)
 * - [ ] Responsive layout
 * - [ ] Comments giải thích logic
 *
 * 📝 Documentation:
 * - README.md:
 *   - Feature overview
 *   - How to use
 *   - Metrics explained
 * - Code comments:
 *   - Tại sao throttle/debounce
 *   - Event listener logic
 *   - State update patterns
 *
 * 🔍 Code Review Self-Checklist:
 * - [ ] Không có memory leaks (mention cần cleanup)
 * - [ ] Events được throttle/debounce đúng cách
 * - [ ] State updates không gây infinite loops
 * - [ ] UI responsive và accessible
 * - [ ] Code readable với comments
 */

// 🎯 STARTER CODE & REQUIREMENTS:

import { useState, useEffect } from 'react';

// Helper: Simple throttle
function throttle(func, delay) {
  let lastCall = 0;
  return function (...args) {
    const now = Date.now();
    if (now - lastCall >= delay) {
      lastCall = now;
      func(...args);
    }
  };
}

// Hoặc version khác dùng apply
// Để nhận 1 mảng làm tham số và định nghĩa ngữ cảnh this
function throttle(func, limit) {
  let inThrottle;
  return function (...args) {
    if (!inThrottle) {
      func.apply(this, args);
      inThrottle = true;
      setTimeout(() => (inThrottle = false), limit);
    }
  };
}

// TODO: Implement TimeTracker
function TimeTracker({ onTimeUpdate }) {
  const [seconds, setSeconds] = useState(0);

  // TODO: useEffect với setInterval
  // Update every second
  // Call onTimeUpdate(seconds) để parent biết

  const formatTime = (secs) => {
    const h = Math.floor(secs / 3600);
    const m = Math.floor((secs % 3600) / 60);
    const s = secs % 60;
    return `${h}h ${m}m ${s}s`;
  };

  return (
    <div className='tracker-card'>
      <h3>⏱️ Time on Page</h3>
      <div className='metric'>{formatTime(seconds)}</div>
    </div>
  );
}

// TODO: Implement MouseTracker
function MouseTracker({ onMouseMove }) {
  const [moves, setMoves] = useState(0);

  useEffect(() => {
    // TODO: Add mousemove listener với throttle
    // Throttle: chỉ count mỗi 100ms
    // Increment moves
    // Call onMouseMove(moves)
    // ⚠️ LƯU Ý: Cần cleanup listener (mention nhưng chưa implement)
  });

  return (
    <div className='tracker-card'>
      <h3>🖱️ Mouse Movements</h3>
      <div className='metric'>{moves}</div>
    </div>
  );
}

// TODO: Implement KeyboardTracker
function KeyboardTracker({ onKeyPress }) {
  // TODO: Track keypress count
  // Add keydown listener
  // Update count
  // Call onKeyPress(count)

  return (
    <div className='tracker-card'>
      <h3>⌨️ Key Presses</h3>
      <div className='metric'>{/* count */}</div>
    </div>
  );
}

// TODO: Implement ScrollTracker
function ScrollTracker({ onScroll }) {
  const [scrollPercent, setScrollPercent] = useState(0);

  useEffect(() => {
    // TODO: Add scroll listener
    // Calculate scroll percentage:
    // (scrollTop / (scrollHeight - clientHeight)) * 100
    // Throttle updates
    // Call onScroll(percent)
  });

  return (
    <div className='tracker-card'>
      <h3>📜 Scroll Depth</h3>
      <div className='metric'>{scrollPercent.toFixed(1)}%</div>
      <div className='progress-bar'>
        <div
          className='progress'
          style={{ width: `${scrollPercent}%` }}
        />
      </div>
    </div>
  );
}

// TODO: Implement VisibilityTracker
function VisibilityTracker({ onVisibilityChange }) {
  const [isVisible, setIsVisible] = useState(true);

  useEffect(() => {
    // TODO: Add visibilitychange listener
    // Update isVisible based on document.hidden
    // Call onVisibilityChange(isVisible)
  });

  return (
    <div className='tracker-card'>
      <h3>👁️ Tab Visibility</h3>
      <div className='metric'>{isVisible ? '✅ Active' : '⏸️ Hidden'}</div>
    </div>
  );
}

// TODO: Implement ActivitySummary
function ActivitySummary({ stats }) {
  // TODO: Determine activity status
  // Active nếu: mouseMoves > 10 OR keyPresses > 5 OR scrollPercent > 0
  // trong vòng last 30 giây

  const isActive = false; // TODO: Calculate

  return (
    <div className='summary-card'>
      <h3>📊 Activity Summary</h3>
      <div className={`status ${isActive ? 'active' : 'idle'}`}>
        {isActive ? '🟢 ACTIVE' : '🔴 IDLE'}
      </div>
      <div className='stats-grid'>
        <div>Time: {/* stats.time */}</div>
        <div>Moves: {/* stats.moves */}</div>
        <div>Keys: {/* stats.keys */}</div>
        <div>Scroll: {/* stats.scroll */}%</div>
      </div>
    </div>
  );
}

// Main Dashboard
function ActivityDashboard() {
  const [stats, setStats] = useState({
    time: 0,
    moves: 0,
    keys: 0,
    scroll: 0,
    isVisible: true,
  });

  // TODO: Implement update handlers
  const handleTimeUpdate = (time) => {
    setStats((prev) => ({ ...prev, time }));
  };

  // TODO: Similar handlers cho moves, keys, scroll, visibility

  const handleReset = () => {
    // TODO: Reset all stats
    // Reload page hoặc reset states
  };

  return (
    <div className='dashboard'>
      <header>
        <h1>📊 Activity Tracker Dashboard</h1>
        <button onClick={handleReset}>🔄 Reset All</button>
      </header>

      <div className='trackers-grid'>
        <TimeTracker onTimeUpdate={handleTimeUpdate} />
        {/* TODO: Render other trackers */}
      </div>

      <ActivitySummary stats={stats} />

      <footer>
        <p>💡 Interact with the page to see metrics update!</p>
        <p>⚠️ Note: This demo does NOT cleanup listeners yet (Ngày 18)</p>
      </footer>
    </div>
  );
}

// 🎨 CSS (Optional - focus on functionality first):
const styles = `
  .dashboard {
    max-width: 1200px;
    margin: 0 auto;
    padding: 2rem;
  }
  
  .trackers-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
    gap: 1rem;
    margin: 2rem 0;
  }
  
  .tracker-card, .summary-card {
    padding: 1.5rem;
    border: 2px solid #ddd;
    border-radius: 8px;
    background: white;
  }
  
  .metric {
    font-size: 2rem;
    font-weight: bold;
    margin: 1rem 0;
  }
  
  .progress-bar {
    height: 8px;
    background: #eee;
    border-radius: 4px;
    overflow: hidden;
  }
  
  .progress {
    height: 100%;
    background: #4CAF50;
    transition: width 0.3s;
  }
  
  .status.active {
    color: #4CAF50;
  }
  
  .status.idle {
    color: #f44336;
  }
`;

// 📋 ACCEPTANCE CRITERIA:
// - [ ] Time tracker ticks mỗi giây
// - [ ] Mouse movements được count (throttled)
// - [ ] Keyboard presses được count
// - [ ] Scroll depth updates khi scroll
// - [ ] Visibility tracker phản ánh tab active/hidden
// - [ ] Activity summary hiển thị status đúng
// - [ ] Reset button clear tất cả metrics
// - [ ] UI responsive và dễ đọc
// - [ ] Code có comments giải thích logic
// - [ ] Không có console errors

export default ActivityDashboard;
💡 Solution
jsx
/**
 * ActivityDashboard - Level 5 Production Challenge
 * Features: Track time on page, mouse movements, key presses, scroll depth, tab visibility
 * Uses throttled event listeners + interval
 * Note: Cleanup functions are commented (to be implemented in Day 18)
 */
import { useState, useEffect } from 'react';

// Simple throttle helper
function throttle(func, limit) {
  let inThrottle;
  return function (...args) {
    if (!inThrottle) {
      func.apply(this, args);
      inThrottle = true;
      setTimeout(() => (inThrottle = false), limit);
    }
  };
}

function TimeTracker({ onTimeUpdate }) {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      setSeconds((s) => {
        const newSeconds = s + 1;
        onTimeUpdate(newSeconds);
        return newSeconds;
      });
    }, 1000);

    // return () => clearInterval(interval); // Day 18 cleanup
  }, []);

  const formatTime = (secs) => {
    const h = Math.floor(secs / 3600);
    const m = Math.floor((secs % 3600) / 60);
    const s = secs % 60;
    return `${h ? h + 'h ' : ''}${m}m ${s}s`.trim();
  };

  return (
    <div
      style={{ border: '1px solid #ddd', padding: '1rem', borderRadius: '8px' }}
    >
      <h3>⏱️ Time on Page</h3>
      <div style={{ fontSize: '1.8rem', fontWeight: 'bold' }}>
        {formatTime(seconds)}
      </div>
    </div>
  );
}

function MouseTracker({ onMouseMove }) {
  const [moves, setMoves] = useState(0);

  useEffect(() => {
    const handleMove = throttle(() => {
      setMoves((m) => {
        const newMoves = m + 1;
        onMouseMove(newMoves);
        return newMoves;
      });
    }, 100);

    window.addEventListener('mousemove', handleMove);
    // return () => window.removeEventListener('mousemove', handleMove); // Day 18
  }, []);

  return (
    <div
      style={{ border: '1px solid #ddd', padding: '1rem', borderRadius: '8px' }}
    >
      <h3>🖱️ Mouse Movements</h3>
      <div style={{ fontSize: '1.8rem', fontWeight: 'bold' }}>{moves}</div>
    </div>
  );
}

function KeyboardTracker({ onKeyPress }) {
  const [keys, setKeys] = useState(0);

  useEffect(() => {
    const handleKey = () => {
      setKeys((k) => {
        const newKeys = k + 1;
        onKeyPress(newKeys);
        return newKeys;
      });
    };

    window.addEventListener('keydown', handleKey);
    // return () => window.removeEventListener('keydown', handleKey); // Day 18
  }, []);

  return (
    <div
      style={{ border: '1px solid #ddd', padding: '1rem', borderRadius: '8px' }}
    >
      <h3>⌨️ Key Presses</h3>
      <div style={{ fontSize: '1.8rem', fontWeight: 'bold' }}>{keys}</div>
    </div>
  );
}

function ScrollTracker({ onScroll }) {
  /*
  * `winScroll`: Vị trí cuộn hiện tại của trang (tính bằng pixel).
  * `height`: Chiều cao khả dụng để cuộn (tính bằng pixel), là tổng chiều cao tài liệu trừ đi chiều cao   *  của cửa sổ trình duyệt.
  * `scrolled`: Phần trăm trang đã được cuộn (tính bằng tỷ lệ `winScroll / height * 100`).
  *  Cập nhật: State `scrollPercent` được cập nhật và callback `onScroll` được gọi mỗi khi có sự kiện cuộn.
  *
  * Giả sử trang có tổng chiều cao là `3000px` và cửa sổ trình duyệt có chiều cao là `1000px`:
  * `height = 3000 - 1000 = 2000px` (là chiều cao có thể cuộn).
  * Nếu người dùng cuộn trang xuống `1000px`, thì `winScroll = 1000px`.
  * `scrolled = (1000 / 2000) * 100 = 50%`.
    Vậy phần trăm cuộn lúc đó là 50%.
  */
  const [scrollPercent, setScrollPercent] = useState(0);

  useEffect(() => {
    const handleScroll = throttle(() => {
      const winScroll = document.documentElement.scrollTop;
      const height =
        document.documentElement.scrollHeight -
        document.documentElement.clientHeight;
      const scrolled = height > 0 ? (winScroll / height) * 100 : 0;

      setScrollPercent(scrolled);
      onScroll(scrolled);
    }, 150);

    window.addEventListener('scroll', handleScroll);
    // return () => window.removeEventListener('scroll', handleScroll); // Day 18
  }, []);

  return (
    <div
      style={{ border: '1px solid #ddd', padding: '1rem', borderRadius: '8px' }}
    >
      <h3>📜 Scroll Depth</h3>
      <div style={{ fontSize: '1.8rem', fontWeight: 'bold' }}>
        {scrollPercent.toFixed(1)}%
      </div>
      <div
        style={{
          height: '8px',
          background: '#eee',
          borderRadius: '4px',
          overflow: 'hidden',
          marginTop: '0.5rem',
        }}
      >
        <div
          style={{
            height: '100%',
            width: `${scrollPercent}%`,
            background: '#4CAF50',
            transition: 'width 0.2s',
          }}
        />
      </div>
    </div>
  );
}

/**
 * @function !document.hidden
 * @description
 * `document.hidden` là một thuộc tính boolean giúp xác định trạng thái của tab trình duyệt.
 * - **`true`**: Trang web bị ẩn (ví dụ: tab không được focus).
 * - **`false`**: Trang web đang hiển thị (tab đang focus).
 *
 * `!document.hidden` đảo ngược giá trị của `document.hidden`, tức là:
 * - **`true`** nếu trang **đang hiển thị**.
 * - **`false`** nếu trang **đang ẩn**.
 */

/**
 * @example
 * Ví dụ 1: Quản lý video khi chuyển tab (TikTok Web)
 * Khi người dùng chuyển tab, video sẽ ngừng phát. Sử dụng `document.hidden` để theo dõi trạng thái của tab.
 */
useEffect(() => {
  /**
   * @function handleVisibilityChange
   * @description
   * Hàm này sẽ được gọi mỗi khi trạng thái visibility thay đổi (tab được focus hoặc ẩn).
   * Nếu tab không còn focus (ẩn), video sẽ dừng. Nếu tab được focus lại, video sẽ tiếp tục phát.
   */
  const handleVisibilityChange = () => {
    if (document.hidden) {
      // Dừng video khi tab không còn focus
      pauseVideo();
    } else {
      // Tiếp tục video khi tab được focus
      playVideo();
    }
  };

  document.addEventListener('visibilitychange', handleVisibilityChange);

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

/**
 * @example
 * Ví dụ 2: Countdown Timer (chờ unlock link download)
 * Nếu người dùng rời tab, đếm ngược sẽ dừng lại. Khi người dùng quay lại, đếm ngược tiếp tục.
 */
useEffect(() => {
  let timer;

  /**
   * @function startCountdown
   * @description
   * Bắt đầu một đếm ngược và dừng lại nếu tab bị ẩn (không focus).
   */
  const startCountdown = () => {
    timer = setInterval(() => {
      if (!document.hidden) {
        // Tiếp tục đếm ngược khi tab đang focus
        updateCountdown();
      } else {
        // Dừng đếm ngược khi tab bị ẩn
        clearInterval(timer);
      }
    }, 1000);
  };

  startCountdown();

  return () => {
    clearInterval(timer);
  };
}, []);

/**
 * @summary
 * - `document.hidden` giúp theo dõi trạng thái tab.
 * - Khi tab bị ẩn, bạn có thể **dừng** video hoặc **dừng đếm ngược**.
 * - Khi tab được focus lại, bạn có thể **tiếp tục** hành động đó.
 */
function VisibilityTracker({ onVisibilityChange }) {
  const [isVisible, setIsVisible] = useState(!document.hidden);

  useEffect(() => {
    const handleVisibility = () => {
      const visible = !document.hidden;
      setIsVisible(visible);
      onVisibilityChange(visible);
    };

    document.addEventListener('visibilitychange', handleVisibility);
    // return () => document.removeEventListener('visibilitychange', handleVisibility); // Day 18
  }, []);

  return (
    <div
      style={{ border: '1px solid #ddd', padding: '1rem', borderRadius: '8px' }}
    >
      <h3>👁️ Tab Visibility</h3>
      <div
        style={{
          fontSize: '1.5rem',
          fontWeight: 'bold',
          color: isVisible ? '#4CAF50' : '#f44336',
        }}
      >
        {isVisible ? '✅ Active' : '⏸️ Hidden'}
      </div>
    </div>
  );
}

function ActivityDashboard() {
  const [stats, setStats] = useState({
    time: 0,
    moves: 0,
    keys: 0,
    scroll: 0,
    isVisible: true,
  });

  const handleTimeUpdate = (time) => setStats((prev) => ({ ...prev, time }));
  const handleMouseUpdate = (moves) => setStats((prev) => ({ ...prev, moves }));
  const handleKeyUpdate = (keys) => setStats((prev) => ({ ...prev, keys }));
  const handleScrollUpdate = (scroll) =>
    setStats((prev) => ({ ...prev, scroll }));
  const handleVisibilityUpdate = (isVisible) =>
    setStats((prev) => ({ ...prev, isVisible }));

  const isActive =
    stats.moves > 5 || stats.keys > 3 || stats.scroll > 1 || stats.time < 10;

  const handleReset = () => {
    setStats({
      time: 0,
      moves: 0,
      keys: 0,
      scroll: 0,
      isVisible: !document.hidden,
    });
  };

  return (
    <div style={{ maxWidth: '1200px', margin: '0 auto', padding: '2rem' }}>
      <header
        style={{
          display: 'flex',
          justifyContent: 'space-between',
          alignItems: 'center',
        }}
      >
        <h1>📊 Activity Tracker Dashboard</h1>
        <button
          onClick={handleReset}
          style={{ padding: '0.6rem 1.2rem', fontSize: '1rem' }}
        >
          🔄 Reset All
        </button>
      </header>

      <div
        style={{
          display: 'grid',
          gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))',
          gap: '1.5rem',
          margin: '2rem 0',
        }}
      >
        <TimeTracker onTimeUpdate={handleTimeUpdate} />
        <MouseTracker onMouseMove={handleMouseUpdate} />
        <KeyboardTracker onKeyPress={handleKeyUpdate} />
        <ScrollTracker onScroll={handleScrollUpdate} />
        <VisibilityTracker onVisibilityChange={handleVisibilityUpdate} />
      </div>

      <div
        style={{
          border: '1px solid #ddd',
          padding: '1.5rem',
          borderRadius: '8px',
          background: '#f9f9f9',
        }}
      >
        <h3>📊 Activity Summary</h3>
        <div
          style={{
            fontSize: '2rem',
            fontWeight: 'bold',
            color: isActive ? '#4CAF50' : '#f44336',
            margin: '1rem 0',
          }}
        >
          {isActive ? '🟢 ACTIVE' : '🔴 IDLE'}
        </div>
        <div
          style={{
            display: 'grid',
            gridTemplateColumns: '1fr 1fr',
            gap: '1rem',
          }}
        >
          <div>Time: {stats.time}s</div>
          <div>Moves: {stats.moves}</div>
          <div>Keys: {stats.keys}</div>
          <div>Scroll: {stats.scroll.toFixed(1)}%</div>
        </div>
      </div>

      <footer style={{ marginTop: '2rem', color: '#666', fontSize: '0.9rem' }}>
        <p>
          Interact with the page (move mouse, type, scroll, switch tabs) to see
          metrics update!
        </p>
        <p>Note: Event listeners cleanup will be added on Day 18</p>
      </footer>
    </div>
  );
}

/*
Kết quả ví dụ khi tương tác:

Mount → Time bắt đầu tăng mỗi giây
Di chuyển chuột → Mouse Movements tăng (throttled ~ mỗi 100ms)
Nhấn phím → Key Presses tăng
Cuộn trang → Scroll Depth thay đổi mượt
Chuyển tab → Tab Visibility chuyển sang "Hidden"
Tất cả thay đổi → Activity Summary cập nhật trạng thái ACTIVE/IDLE
Click Reset → Tất cả về 0, time tiếp tục đếm từ đầu
*/

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

Bảng So Sánh: useEffect vs Direct Execution

Tiêu chíDirect trong RenderuseEffectEvent Handler
TimingTRONG quá trình renderSAU khi render xongKhi user action xảy ra
BlockingBlock renderingKhông blockKhông block
Use caseTính toán UISide effects sync với stateRespond to user
Ví dụDerived statedocument.title, fetchonClick, onSubmit
RunsMỗi lần renderTheo dependenciesKhi được trigger
PerformanceCó thể chậm renderKhông ảnh hưởng renderOn-demand

Bảng So Sánh: useEffect với/không Dependencies

Tiêu chíKhông có depsdeps = []deps = [a, b]
SyntaxuseEffect(() => {})useEffect(() => {}, [])useEffect(() => {}, [a, b])
Chạy khiMỖI renderCHỈ mount (1 lần)Khi a hoặc b thay đổi
Ngày họcNgày 16 ✅Ngày 17Ngày 17
Use caseLog mọi renderSetup 1 lầnSync với specific state
Vấn đềChạy quá nhiềuKhông update khi cầnStale closure (Ngày 17)

⚠️ LƯU Ý QUAN TRỌNG:

  • Hôm nay (Ngày 16) chỉ học useEffect KHÔNG có dependencies
  • Ngày 17 sẽ học dependencies array chi tiết
  • Ngày 18 sẽ học cleanup function

Decision Tree: Khi nào dùng useEffect?

Bạn cần làm gì đó trong component?

├─ Tính toán từ props/state để render UI?
│  → KHÔNG dùng useEffect, tính trực tiếp (Derived State)
│  → Ví dụ: const fullName = firstName + " " + lastName;

├─ Respond to user action? (click, type, submit)
│  → KHÔNG dùng useEffect, dùng Event Handler
│  → Ví dụ: onClick={() => handleClick()}

├─ Làm gì đó BÊN NGOÀI component? (Side Effect)
│  │
│  ├─ Cần chạy SAU MỖI render?
│  │  → useEffect(() => { ... })  // No deps
│  │  → Ví dụ: Log mọi render
│  │
│  ├─ Cần chạy 1 LẦN khi mount?
│  │  → useEffect(() => { ... }, [])  // Empty deps (Ngày 17)
│  │  → Ví dụ: Fetch initial data
│  │
│  └─ Cần chạy khi SPECIFIC value thay đổi?
│     → useEffect(() => { ... }, [value])  // With deps (Ngày 17)
│     → Ví dụ: Update document.title khi count thay đổi

└─ Không chắc?
   → Hỏi: "Điều này có ảnh hưởng thứ bên ngoài component không?"
   → Nếu YES → useEffect
   → Nếu NO → Tính trực tiếp hoặc event handler

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

Bug #1: Infinite Loop 🔥

jsx
/**
 * 🐛 BUG: Component re-render vô hạn
 * 🎯 Nhiệm vụ: Tìm lỗi và sửa
 */

function BuggyCounter() {
  const [count, setCount] = useState(0);
  const [double, setDouble] = useState(0);

  // ❌ BUG: Vòng lặp vô hạn!
  useEffect(() => {
    setDouble(count * 2);
  });

  return (
    <div>
      <p>Count: {count}</p>
      <p>Double: {double}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  );
}

// 🤔 CÂU HỎI DEBUG:
// 1. Tại sao component re-render vô hạn?
// 2. Effect chạy khi nào?
// 3. setDouble() làm gì?

// 💡 GIẢI THÍCH:
// - useEffect (no deps) chạy SAU MỖI render
// - setDouble() trigger re-render
// - Re-render → useEffect chạy lại → setDouble() → Re-render → ...
// → INFINITE LOOP!

// ✅ CÁCH SỬA (với kiến thức Ngày 16):
function FixedCounter() {
  const [count, setCount] = useState(0);

  // ✅ Tính trực tiếp, KHÔNG dùng useEffect cho derived state
  const double = count * 2;

  return (
    <div>
      <p>Count: {count}</p>
      <p>Double: {double}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  );
}

// 🎓 BÀI HỌC:
// - useEffect cho side effects, KHÔNG cho derived state
// - Calling setState trong effect → Cẩn thận với infinite loops!
// - Ngày 17 sẽ học dependencies array để control khi nào effect chạy

Bug #2: Effect trong Event Handler 🤔

jsx
/**
 * 🐛 BUG: Lẫn lộn useEffect và event handler
 * 🎯 Nhiệm vụ: Sửa logic
 */

function BuggyForm() {
  const [name, setName] = useState('');
  const [submitted, setSubmitted] = useState(false);

  // ❌ BUG: Dùng useEffect cho user action!
  useEffect(() => {
    if (name.length > 0) {
      // Effect chạy MỖI khi name thay đổi, KHÔNG phải khi submit!
      setSubmitted(true);
      console.log('Form submitted:', name);
    }
  });

  return (
    <div>
      <input
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder='Your name'
      />
      {/* Button không làm gì cả! */}
      <button>Submit</button>
      {submitted && <p>Thanks {name}!</p>}
    </div>
  );
}

// 🤔 CÂU HỎI DEBUG:
// 1. Khi nào "Form submitted" được log?
// 2. Button "Submit" có được dùng không?
// 3. Logic submit có đúng không?

// 💡 GIẢI THÍCH:
// - Effect chạy mỗi khi `name` thay đổi (mỗi keystroke!)
// - Button onClick không được định nghĩa
// - Logic submit chạy sai thời điểm

// ✅ CÁCH SỬA:
function FixedForm() {
  const [name, setName] = useState('');
  const [submitted, setSubmitted] = useState(false);

  // ✅ Dùng event handler cho user action
  const handleSubmit = () => {
    if (name.length > 0) {
      setSubmitted(true);
      console.log('Form submitted:', name);
    }
  };

  return (
    <div>
      <input
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder='Your name'
      />
      <button onClick={handleSubmit}>Submit</button>
      {submitted && <p>Thanks {name}!</p>}
    </div>
  );
}

// 🎓 BÀI HỌC:
// - useEffect: Cho side effects SYNC với state (auto)
// - Event handlers: Cho side effects từ USER ACTIONS (manual)
// - KHÔNG dùng effect cho logic respond to clicks/submissions!

Bug #3: Multiple State Updates 🚨

jsx
/**
 * 🐛 BUG: Performance issue với nhiều state updates
 * 🎯 Nhiệm vụ: Optimize
 */

function BuggyWeatherWidget() {
  const [temp, setTemp] = useState(20);
  const [humidity, setHumidity] = useState(50);
  const [condition, setCondition] = useState('Sunny');

  // ❌ BUG: 3 effects riêng biệt, mỗi cái trigger re-render
  useEffect(() => {
    document.title = `${temp}°C`;
  });

  useEffect(() => {
    document.title = `${temp}°C, ${humidity}%`;
  });

  useEffect(() => {
    document.title = `${temp}°C, ${humidity}%, ${condition}`;
  });

  const updateWeather = () => {
    setTemp(25); // → Re-render + 3 effects run
    setHumidity(60); // → Re-render + 3 effects run
    setCondition('Cloudy'); // → Re-render + 3 effects run
    // Total: 3 re-renders, 9 effect executions!
  };

  return (
    <div>
      <p>
        {temp}°C, {humidity}%, {condition}
      </p>
      <button onClick={updateWeather}>Update Weather</button>
    </div>
  );
}

// 🤔 CÂU HỎI DEBUG:
// 1. Bao nhiêu lần component re-render khi click button?
// 2. Bao nhiêu lần mỗi effect chạy?
// 3. document.title cuối cùng là gì?

// 💡 GIẢI THÍCH:
// - 3 setState → 3 re-renders (React batches trong event handler, nhưng vẫn 3 renders)
// - Mỗi render → 3 effects chạy
// - Effect 3 ghi đè Effect 1 và 2
// - Lãng phí performance!

// ✅ CÁCH SỬA #1: Gộp vào 1 effect
function FixedWeatherV1() {
  const [temp, setTemp] = useState(20);
  const [humidity, setHumidity] = useState(50);
  const [condition, setCondition] = useState('Sunny');

  // ✅ 1 effect duy nhất
  useEffect(() => {
    document.title = `${temp}°C, ${humidity}%, ${condition}`;
  });

  const updateWeather = () => {
    setTemp(25);
    setHumidity(60);
    setCondition('Cloudy');
  };

  return (
    <div>
      <p>
        {temp}°C, {humidity}%, {condition}
      </p>
      <button onClick={updateWeather}>Update Weather</button>
    </div>
  );
}

// ✅ CÁCH SỬA #2: Group state (BEST)
function FixedWeatherV2() {
  const [weather, setWeather] = useState({
    temp: 20,
    humidity: 50,
    condition: 'Sunny',
  });

  // ✅ 1 state, 1 effect
  useEffect(() => {
    document.title = `${weather.temp}°C, ${weather.humidity}%, ${weather.condition}`;
  });

  const updateWeather = () => {
    // 1 setState → 1 re-render → 1 effect run
    setWeather({
      temp: 25,
      humidity: 60,
      condition: 'Cloudy',
    });
  };

  return (
    <div>
      <p>
        {weather.temp}°C, {weather.humidity}%, {weather.condition}
      </p>
      <button onClick={updateWeather}>Update Weather</button>
    </div>
  );
}

// 🎓 BÀI HỌC:
// - Minimize số lượng effects
// - Group related state together
// - 1 effect có thể access nhiều state variables
// - Performance: Fewer re-renders = Better UX

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

Knowledge Check

Đánh dấu ✅ những điều bạn đã hiểu:

Concepts:

  • [ ] Tôi hiểu side effect là gì
  • [ ] Tôi biết tại sao không nên làm side effects trong render
  • [ ] Tôi hiểu useEffect chạy SAU khi DOM đã update
  • [ ] Tôi biết phân biệt useEffect và event handlers
  • [ ] Tôi hiểu effect không có dependencies chạy mỗi render

Practices:

  • [ ] Tôi có thể dùng useEffect để update document.title
  • [ ] Tôi có thể dùng useEffect để log state changes
  • [ ] Tôi biết khi nào NÊN dùng useEffect
  • [ ] Tôi biết khi nào KHÔNG NÊN dùng useEffect
  • [ ] Tôi tránh được infinite loops cơ bản

Debugging:

  • [ ] Tôi nhận biết được khi effect chạy quá nhiều lần
  • [ ] Tôi hiểu tại sao setState trong effect có thể gây vòng lặp
  • [ ] Tôi biết cách debug bằng console.log trong effect
  • [ ] Tôi có thể trace effect execution order

Code Review Checklist

Khi review code có useEffect, kiểm tra:

Timing & Logic:

  • [ ] Effect chạy đúng thời điểm (SAU render, không phải TRONG render)
  • [ ] Logic trong effect là side effect, KHÔNG phải derived state
  • [ ] Không có infinite loops (setState trong effect phải cẩn thận)

Best Practices:

  • [ ] Effect code rõ ràng, dễ hiểu
  • [ ] Comments giải thích TẠI SAO cần effect
  • [ ] Không có duplicate effects (gộp logic tương tự)
  • [ ] Effect đơn giản, không quá phức tạp

Performance:

  • [ ] Minimize số lượng effects
  • [ ] Group related state để reduce re-renders
  • [ ] Effect không chạy không cần thiết

Future-proof:

  • [ ] Comments mention sẽ cần cleanup (Ngày 18)
  • [ ] Chuẩn bị cho dependencies array (Ngày 17)

🏠 BÀI TẬP VỀ NHÀ

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

Bài 1: Greeting Message với Time of Day

jsx
/**
 * Tạo component hiển thị lời chào dựa trên giờ:
 * - 5-12h: "Good Morning"
 * - 12-18h: "Good Afternoon"
 * - 18-22h: "Good Evening"
 * - 22-5h: "Good Night"
 *
 * Requirements:
 * - Dùng useEffect để update message mỗi phút
 * - Update document.title với message
 * - Hiển thị current time
 *
 * Hints:
 * - new Date().getHours() để lấy giờ
 * - setInterval để update mỗi 60 giây
 */
💡 Solution
jsx
/**
 * GreetingWithTime - Bài tập về nhà Bắt buộc Bài 1
 * Hiển thị lời chào theo khung giờ + thời gian hiện tại
 * Cập nhật mỗi phút bằng setInterval trong useEffect
 * Đồng bộ document.title với lời chào
 */
function GreetingWithTime() {
  const [greeting, setGreeting] = useState('');
  const [currentTime, setCurrentTime] = useState('');

  const updateGreetingAndTime = () => {
    const now = new Date();
    const hours = now.getHours();
    const minutes = now.getMinutes();
    const seconds = now.getSeconds();

    // Format thời gian HH:MM:SS
    const timeStr = `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;

    // Xác định lời chào
    let message = '';
    if (hours >= 5 && hours < 12) {
      message = 'Good Morning';
    } else if (hours >= 12 && hours < 18) {
      message = 'Good Afternoon';
    } else if (hours >= 18 && hours < 22) {
      message = 'Good Evening';
    } else {
      message = 'Good Night';
    }

    setGreeting(message);
    setCurrentTime(timeStr);

    // Update document title
    document.title = `${message} - ${timeStr}`;
  };

  useEffect(() => {
    // Chạy ngay lập tức khi mount
    updateGreetingAndTime();

    // Cập nhật mỗi 60 giây
    const interval = setInterval(() => {
      updateGreetingAndTime();
    }, 60000);

    // Note: Cleanup sẽ học ở Ngày 18
    // return () => clearInterval(interval);
  }, []);

  return (
    <div>
      <h2>{greeting}</h2>
      <p style={{ fontSize: '1.5rem', fontWeight: 'bold' }}>
        Current time: {currentTime}
      </p>
      <p style={{ color: '#666', fontSize: '0.9rem' }}>
        Message và title sẽ tự động cập nhật mỗi phút
      </p>
    </div>
  );
}

/*
Kết quả ví dụ (giả sử giờ hiện tại là 09:45:23):

→ Hiển thị: Good Morning
→ Current time: 09:45:23
→ document.title: "Good Morning - 09:45:23"

Sau 1 phút (10:45:23):
→ Good Morning
→ Current time: 10:45:23
→ document.title: "Good Morning - 10:45:23"

Khi qua 12:00:
→ Good Afternoon
→ document.title cập nhật tương ứng
*/

Bài 2: Click Counter với Stats

jsx
/**
 * Tạo component track clicks:
 * - Đếm total clicks
 * - Đếm clicks trong 5 giây gần nhất (recent clicks)
 * - Tính average clicks per second
 *
 * Requirements:
 * - useEffect để log stats mỗi khi click
 * - Display all stats
 * - Reset button
 *
 * Hints:
 * - Store click timestamps trong array
 * - Filter clicks trong 5s: now - timestamp < 5000
 */
💡 Solution
jsx
/**
 * ClickCounterWithStats - Bài tập về nhà Bắt buộc Bài 2
 * Theo dõi số lần click:
 * - Total clicks
 * - Recent clicks (trong 5 giây gần nhất)
 * - Average clicks per second (dựa trên total clicks / thời gian chạy)
 * Log stats mỗi khi click bằng useEffect
 * Có nút Reset để xóa hết dữ liệu
 */
function ClickCounterWithStats() {
  const [clickTimestamps, setClickTimestamps] = useState([]);
  const [startTime, setStartTime] = useState(Date.now());

  // Mỗi lần click → thêm timestamp
  const handleClick = () => {
    const now = Date.now();
    setClickTimestamps((prev) => [...prev, now]);
  };

  // useEffect log stats mỗi khi clickTimestamps thay đổi
  useEffect(() => {
    if (clickTimestamps.length === 0) return;

    const now = Date.now();
    const totalClicks = clickTimestamps.length;

    // Recent clicks: trong 5 giây gần nhất
    const recentClicks = clickTimestamps.filter(
      (ts) => now - ts <= 5000,
    ).length;

    // Average clicks per second (từ lúc bắt đầu đến giờ)
    const elapsedSeconds = (now - startTime) / 1000;
    const avgCPS =
      elapsedSeconds > 0 ? (totalClicks / elapsedSeconds).toFixed(2) : 0;

    console.log('Click stats:', {
      totalClicks,
      recentClicks,
      averageCPS: `${avgCPS} clicks/sec`,
      timestamp: new Date().toISOString(),
    });
  }, [clickTimestamps]);

  const handleReset = () => {
    setClickTimestamps([]);
    setStartTime(Date.now());
  };

  // Tính toán hiển thị
  const now = Date.now();
  const totalClicks = clickTimestamps.length;
  const recentClicks = clickTimestamps.filter((ts) => now - ts <= 5000).length;
  const elapsedSeconds = (now - startTime) / 1000;
  const avgCPS =
    elapsedSeconds > 0 ? (totalClicks / elapsedSeconds).toFixed(2) : '0.00';

  return (
    <div>
      <h2>Click Counter with Stats</h2>

      <button
        onClick={handleClick}
        style={{ padding: '1rem 2rem', fontSize: '1.5rem', margin: '1rem 0' }}
      >
        Click Me!
      </button>

      <div style={{ margin: '1rem 0', fontSize: '1.2rem' }}>
        <p>
          <strong>Total clicks:</strong> {totalClicks}
        </p>
        <p>
          <strong>Recent clicks (last 5s):</strong> {recentClicks}
        </p>
        <p>
          <strong>Average:</strong> {avgCPS} clicks/second
        </p>
        <p>
          <strong>Time running:</strong> {Math.floor(elapsedSeconds)} seconds
        </p>
      </div>

      <button
        onClick={handleReset}
        style={{ padding: '0.6rem 1.2rem', fontSize: '1rem' }}
      >
        Reset All
      </button>

      <p style={{ color: '#666', marginTop: '1.5rem', fontSize: '0.9rem' }}>
        Mở console để xem log chi tiết mỗi lần click
      </p>
    </div>
  );
}

/*
Kết quả ví dụ khi tương tác:

Click 5 lần nhanh trong 2 giây:
→ Console log mỗi lần:
  { totalClicks: 1, recentClicks: 1, averageCPS: "0.50 clicks/sec", ... }
  { totalClicks: 2, recentClicks: 2, averageCPS: "1.00 clicks/sec", ... }
  ...
  { totalClicks: 5, recentClicks: 5, averageCPS: "2.50 clicks/sec", ... }

Sau 10 giây không click:
→ recentClicks giảm dần về 0 (vì timestamp cũ hơn 5s bị loại)

Click tiếp → recentClicks tăng lại, averageCPS tính trên toàn bộ thời gian từ lúc reset
*/

Nâng cao (60 phút)

Bài 3: Multi-Tab Sync

jsx
/**
 * Tạo app sync state giữa nhiều tabs:
 * - Input field cho message
 * - useEffect để log khi message thay đổi
 * - Display message
 * - Bonus: Thử dùng localStorage (tự research)
 *
 * Test:
 * - Mở 2 tabs cùng lúc
 * - Nhập message ở tab 1
 * - Quan sát console ở cả 2 tabs
 *
 * Challenges:
 * - localStorage.setItem() trong effect
 * - window.addEventListener('storage') (tự tìm hiểu)
 */
💡 Solution
jsx
/**
 * MultiTabSync - Bài tập về nhà Nâng cao Bài 3
 * Đồng bộ message giữa các tab trình duyệt qua localStorage
 * - Input để nhập message
 * - Hiển thị message hiện tại
 * - useEffect: log mỗi khi message thay đổi
 * - useEffect: lưu vào localStorage khi message thay đổi
 * - useEffect: lắng nghe sự kiện 'storage' để sync từ tab khác
 */
function MultiTabSync() {
  const [message, setMessage] = useState(() => {
    // Khởi tạo từ localStorage nếu có
    return localStorage.getItem('sharedMessage') || '';
  });

  // Log mỗi khi message thay đổi
  useEffect(() => {
    console.log(
      `Message updated: "${message}" (at ${new Date().toLocaleTimeString()})`,
    );
  }, [message]);

  // Lưu message vào localStorage mỗi khi thay đổi
  useEffect(() => {
    localStorage.setItem('sharedMessage', message);
  }, [message]);

  // Lắng nghe sự kiện storage từ các tab khác
  useEffect(() => {
    const handleStorageChange = (event) => {
      if (event.key === 'sharedMessage') {
        setMessage(event.newValue || '');
        console.log(
          `Message synced from another tab: "${event.newValue}" ` +
            `(from tab at ${new Date().toLocaleTimeString()})`,
        );
      }
    };

    window.addEventListener('storage', handleStorageChange);

    // Note: Cleanup sẽ học chi tiết ở Ngày 18
    // return () => window.removeEventListener('storage', handleStorageChange);

    // Trigger initial sync nếu có giá trị từ trước
    const stored = localStorage.getItem('sharedMessage');
    if (stored !== message) {
      setMessage(stored || '');
    }
  }, []);

  const handleChange = (e) => {
    setMessage(e.target.value);
  };

  const handleClear = () => {
    setMessage('');
  };

  return (
    <div>
      <h2>Multi-Tab Message Sync</h2>
      <p style={{ color: '#555', marginBottom: '1.5rem' }}>
        Mở 2+ tab cùng trang này → nhập message ở tab này → xem console và
        message ở tab khác tự động cập nhật
      </p>

      <input
        type='text'
        value={message}
        onChange={handleChange}
        placeholder='Nhập message để sync giữa các tab...'
        style={{ width: '100%', padding: '0.8rem', fontSize: '1.1rem' }}
      />

      <div
        style={{ margin: '1.5rem 0', fontSize: '1.3rem', fontWeight: 'bold' }}
      >
        Current message: {message || '(empty)'}
      </div>

      <button
        onClick={handleClear}
        style={{ padding: '0.6rem 1.2rem', fontSize: '1rem' }}
      >
        Clear Message
      </button>

      <p style={{ marginTop: '2rem', color: '#666', fontSize: '0.9rem' }}>
        Cách hoạt động:
        <br />• Mỗi lần gõ → lưu vào localStorage
        <br />• Tab khác nhận sự kiện 'storage' → cập nhật state
        <br />• Console log cả 2 tab để theo dõi
      </p>
    </div>
  );
}

/*
Kết quả ví dụ khi test với 2 tab:

Tab 1: Gõ "Hello from Tab 1"
→ Console Tab 1: Message updated: "Hello from Tab 1" ...
→ localStorage cập nhật

Tab 2 (mở cùng lúc):
→ Tự động hiển thị: "Hello from Tab 1"
→ Console Tab 2: Message synced from another tab: "Hello from Tab 1" ...

Tab 2: Gõ "Hi back from Tab 2"
→ Tab 1 tự động cập nhật message
→ Cả hai tab đều log sự thay đổi

Click Clear ở tab nào → cả hai tab đều về empty
*/

Bài 4: Performance Monitor

jsx
/**
 * Tạo component monitor component re-renders:
 * - Track số lần component render
 * - Track số lần mỗi effect chạy
 * - Display stats
 * - Multiple states để trigger re-renders
 *
 * Requirements:
 * - useEffect để count effect runs
 * - Compare với render count
 * - Optimize để reduce unnecessary effects
 *
 * Goal:
 * - Hiểu mối quan hệ giữa renders và effects
 * - Practice minimizing effects
 */
💡 Solution
jsx
/**
 * RenderAndEffectMonitor - Bài tập về nhà Nâng cao Bài 4
 * Track render count bằng state + useEffect (cách tạm chấp nhận được)
 * Track số lần mỗi effect chạy
 * Hiển thị so sánh giữa render count và effect executions
 */
function RenderAndEffectMonitor() {
  // State để trigger re-renders
  const [countA, setCountA] = useState(0);
  const [countB, setCountB] = useState(0);
  const [countC, setCountC] = useState(0);

  // Track render count bằng state (cập nhật trong useEffect)
  const [renderCount, setRenderCount] = useState(0);

  // Track effect executions
  const [effect1Runs, setEffect1Runs] = useState(0);
  const [effect2Runs, setEffect2Runs] = useState(0);
  const [effect3Runs, setEffect3Runs] = useState(0);

  // Effect theo dõi render count (chạy sau mỗi render)
  useEffect(() => {
    setRenderCount((prev) => prev + 1);
  });

  // Effect 1: Chạy mỗi render (no deps) - BAD practice
  useEffect(() => {
    setEffect1Runs((r) => r + 1);
    console.log('Effect 1 (no deps) ran');
  });

  // Effect 2: Chỉ chạy khi countA thay đổi
  useEffect(() => {
    setEffect2Runs((r) => r + 1);
    console.log('Effect 2 (deps: [countA]) ran');
  }, [countA]);

  // Effect 3: Chỉ chạy khi countB hoặc countC thay đổi
  useEffect(() => {
    setEffect3Runs((r) => r + 1);
    console.log('Effect 3 (deps: [countB, countC]) ran');
  }, [countB, countC]);

  const incrementA = () => setCountA((a) => a + 1);
  const incrementB = () => setCountB((b) => b + 1);
  const incrementC = () => setCountC((c) => c + 1);

  const resetAll = () => {
    setCountA(0);
    setCountB(0);
    setCountC(0);
    setEffect1Runs(0);
    setEffect2Runs(0);
    setEffect3Runs(0);
    setRenderCount(0);
  };

  return (
    <div>
      <h2>Render & Effect Monitor</h2>

      <div
        style={{
          margin: '1.5rem 0',
          padding: '1rem',
          background: '#f0f8ff',
          borderRadius: '8px',
        }}
      >
        <h3>Render Statistics</h3>
        <p>
          <strong>Total renders:</strong> {renderCount}
        </p>
        <p>
          <strong>Effect 1 runs (no deps):</strong> {effect1Runs}
        </p>
        <p>
          <strong>Effect 2 runs (deps: countA):</strong> {effect2Runs}
        </p>
        <p>
          <strong>Effect 3 runs (deps: countB, countC):</strong> {effect3Runs}
        </p>
      </div>

      <div style={{ margin: '2rem 0' }}>
        <h3>Trigger Re-renders</h3>
        <div style={{ display: 'flex', gap: '1rem', flexWrap: 'wrap' }}>
          <button onClick={incrementA}>Increment A ({countA})</button>
          <button onClick={incrementB}>Increment B ({countB})</button>
          <button onClick={incrementC}>Increment C ({countC})</button>
        </div>
      </div>

      <button
        onClick={resetAll}
        style={{
          padding: '0.8rem 1.5rem',
          fontSize: '1rem',
          marginTop: '1rem',
        }}
      >
        Reset All Counters
      </button>

      <div
        style={{
          marginTop: '2rem',
          padding: '1rem',
          background: '#fff3cd',
          borderRadius: '8px',
        }}
      >
        <h4>Lưu ý quan trọng:</h4>
        <ul style={{ margin: 0, paddingLeft: '1.5rem' }}>
          <li>
            Cách đếm render bằng state + useEffect(no deps) sẽ làm tăng thêm 1
            render mỗi lần → không chính xác 100%
          </li>
          <li>Nhưng đủ để minh họa: effect không deps chạy ≈ số render</li>
          <li>
            Khi học useRef → sẽ thay bằng ref.current += 1 (không gây re-render
            thêm)
          </li>
          <li>
            Mục tiêu chính: thấy rõ effect(no deps) chạy quá nhiều → cần deps
            array
          </li>
        </ul>
      </div>
    </div>
  );
}

/*
Kết quả ví dụ khi tương tác:

Mount → renderCount ≈ 2 (1 mount + 1 từ setRenderCount), effect1 ≈ 2

Click Increment A 3 lần:
→ Mỗi lần: renderCount tăng ~2 (vì setRenderCount gây render thêm)
   effect1 tăng ~2, effect2 tăng 1, effect3 không đổi

→ Sau 3 click: renderCount ≈ 8, effect1 ≈ 8, effect2 ≈ 3, effect3 ≈ 0

→ Minh họa: effect không deps chạy gần bằng số render → rất tốn kém
   → Nên dùng deps array để giới hạn khi nào effect thực sự cần chạy
*/

📚 TÀI LIỆU THAM KHẢO

Bắt buộc đọc

  1. React Docs - useEffect

  2. You Might Not Need an Effect

Đọc thêm

  1. A Complete Guide to useEffect (Dan Abramov)

  2. The Lifecycle of Effects


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

Kiến thức nền (đã học):

  • Ngày 11-12: useState fundamentals & patterns

    • Đã học: State updates trigger re-renders
    • Kết nối: useEffect chạy SAU re-render
  • Ngày 13: Forms với State

    • Đã học: Controlled components
    • Kết nối: useEffect có thể sync form state với external systems
  • Ngày 14: Lifting State Up

    • Đã học: Props drilling, callback props
    • Kết nối: useEffect có thể notify parent về state changes

Hướng tới (sẽ học):

  • Ngày 17: useEffect Dependencies Deep Dive

    • Sẽ học: Dependencies array [a, b]
    • Sẽ học: Khi nào effect chạy lại
    • Sẽ học: Stale closure problems
  • Ngày 18: Cleanup & Memory Leaks

    • Sẽ học: Return cleanup function
    • Sẽ học: Prevent memory leaks
    • Sẽ học: Event listener cleanup
  • Ngày 19-20: Data Fetching

    • Sẽ học: fetch API trong useEffect
    • Sẽ học: Loading/Error states
    • Sẽ học: AbortController

💡 SENIOR INSIGHTS

Cân Nhắc Production

1. Performance Impact:

jsx
// ❌ BAD: Effect chạy quá nhiều
function Bad() {
  const [a, setA] = useState(0);
  const [b, setB] = useState(0);

  useEffect(() => {
    /* expensive operation */
  });
  useEffect(() => {
    /* expensive operation */
  });

  // Mỗi setState → 2 expensive effects run!
}

// ✅ GOOD: Minimize effects
function Good() {
  const [state, setState] = useState({ a: 0, b: 0 });

  useEffect(() => {
    /* 1 expensive operation */
  });

  // 1 setState → 1 effect run
}

2. Debugging Strategy:

jsx
useEffect(() => {
  // ✅ ALWAYS log when effect runs trong development
  if (process.env.NODE_ENV === 'development') {
    console.log('Effect ran:', {
      timestamp: Date.now(),
      values: {
        /* relevant state */
      },
    });
  }

  // Your effect logic
});

3. Code Organization:

jsx
// ✅ GOOD: Multiple focused effects
function Component() {
  // Effect 1: Document title
  useEffect(() => {
    document.title = `${title}`;
  });

  // Effect 2: Analytics
  useEffect(() => {
    trackPageView(page);
  });

  // Dễ đọc, dễ maintain hơn 1 effect khổng lồ
}

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

Junior Level:

  1. Q: useEffect chạy khi nào?

    A: useEffect chạy SAU khi component render và DOM đã được update. Nó chạy SAU khi user đã nhìn thấy UI, không block browser painting.

  2. Q: Tại sao không nên làm side effects trực tiếp trong render?

    A: Vì:

    • Render function phải pure (predictable output từ inputs)
    • React có thể gọi render nhiều lần
    • Side effects cần kiểm soát timing
    • useEffect cho phép React control KHI NÀO side effects chạy
  3. Q: Phân biệt useEffect và event handler?

    A:

    • useEffect: Cho side effects SYNC với state (auto, sau render)
    • Event handlers: Cho side effects từ USER ACTIONS (manual, khi user tương tác)
    • Ví dụ: onClick → event handler, update title khi state thay đổi → useEffect

Mid Level:

  1. Q: useEffect không có dependencies array khác gì có empty array []?

    A:

    • No deps: Chạy SAU MỖI render
    • Empty deps []: Chỉ chạy 1 lần SAU mount (Ngày 17)
    • With deps [a, b]: Chạy khi a hoặc b thay đổi (Ngày 17)
  2. Q: Làm sao tránh infinite loop với useEffect?

    A:

    • KHÔNG call setState trong effect mà KHÔNG có dependencies control
    • Dùng dependencies array (Ngày 17) để limit khi nào effect chạy
    • Derived state thay vì effect + setState
    • Debug bằng cách log và track re-renders

Senior Level:

  1. Q: Khi nào bạn KHÔNG nên dùng useEffect?

    A:

    • Tính toán derived state (tính trực tiếp)
    • Transform data cho rendering (dùng useMemo sau này)
    • Handle user events (dùng event handlers)
    • Initialize state (dùng useState với initial value hoặc lazy init)
    • Chain state updates (batch trong event handler)
  2. Q: Thiết kế effect strategy cho feature phức tạp?

    A:

    • Tách effects theo concern (title, analytics, sync, etc.)
    • Group related state để reduce effects
    • Document WHY mỗi effect cần thiết
    • Plan cleanup (Ngày 18)
    • Consider custom hooks để reuse logic (Ngày 24)

War Stories

Story #1: The Infinite Loop Nightmare 🔥

"Tháng đầu làm React, tôi tạo một form với useEffect để validate input. Tôi setState error message trong effect mà không có dependencies. Kết quả? Browser freeze, React hiển thị warning 'Maximum update depth exceeded'. Mất 2 tiếng debug mới nhận ra effect chạy mỗi render → setState → re-render → effect chạy lại... Từ đó, tôi luôn cẩn thận với setState trong effects!"

Story #2: Performance Debugging

"Production app chạy chậm, Chrome DevTools chỉ ra component re-render 50 lần/giây. Thủ phạm? 5 effects riêng biệt track different stats, mỗi effect trigger state update → cascade re-renders. Solution: Gộp state vào 1 object, reduce từ 5 effects xuống 1. Performance boost 10x. Bài học: Measure trước khi optimize, nhưng cũng đừng tạo quá nhiều effects ngay từ đầu."

Story #3: Effect vs Event Handler Confusion

"Junior dev trong team dùng useEffect để handle button click. Code chạy, nhưng logic sai: effect chạy SAU render, không phải KHI click. Kết quả: Feature bug, user clicks không được process đúng. Phải explain: useEffect cho sync với state, event handlers cho user actions. Từ đó, team có convention: 'If user triggers it, use onClick. If state triggers it, use useEffect.'"


🎯 NGÀY MAI: useEffect - Dependencies Deep Dive

Preview những gì bạn sẽ học:

Dependencies Array [][a, b]

  • Kiểm soát KHI NÀO effect chạy
  • Empty deps: Chỉ 1 lần sau mount
  • Specific deps: Chạy khi deps thay đổi

Stale Closure Problem

  • Closures trong effects
  • Tại sao values có thể "cũ"
  • Functional updates trong effects

ESLint exhaustive-deps

  • Linter rule giúp avoid bugs
  • Tại sao phải khai báo đầy đủ dependencies
  • Exceptions và workarounds

Best Practices

  • Dependency optimization
  • Object/Array dependencies
  • Custom comparison

🔥 Chuẩn bị:

  • Ôn lại closures trong JavaScript
  • Làm xong bài tập về nhà hôm nay
  • Suy nghĩ về các vấn đề của useEffect không có deps

🎉 Chúc mừng! Bạn đã hoàn thành Ngày 16!

Bạn đã:

  • ✅ Hiểu được Side Effects và tại sao cần useEffect
  • ✅ Sử dụng được useEffect basic (no dependencies)
  • ✅ Phân biệt được useEffect vs Event Handlers
  • ✅ Tránh được các lỗi phổ biến (infinite loops, wrong timing)
  • ✅ Áp dụng được useEffect vào real-world scenarios

Tiếp tục giữ đà! Ngày 17 sẽ mở ra nhiều patterns mạnh mẽ hơn! 🚀

Personal tech knowledge base