Skip to content

📅 NGÀY 38: Context Performance Optimization

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

  • [ ] Hiểu tại sao Context gây performance issues
  • [ ] Biết cách profile và detect re-render problems
  • [ ] Nắm vững useMemo/useCallback cho Context value
  • [ ] Hiểu Context Splitting pattern (State + Dispatch)
  • [ ] Biết implement Selector pattern đơn giản
  • [ ] Biết khi nào NÊN và KHÔNG NÊN optimize

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

  1. Object reference equality là gì? {} === {} trả về gì?
  2. useMemo và useCallback khác nhau như thế nào?
  3. React.memo làm gì? Khi nào component re-render?

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

1.1 Vấn Đề Thực Tế

Context rất tiện, nhưng có performance cost:

jsx
/**
 * ❌ PROBLEM: Context Performance Issue
 */

const AppContext = createContext();

function AppProvider({ children }) {
  const [user, setUser] = useState(null);
  const [theme, setTheme] = useState('light');
  const [notifications, setNotifications] = useState([]);

  // ❌ NEW OBJECT mỗi render!
  const value = {
    user,
    setUser,
    theme,
    setTheme,
    notifications,
    setNotifications,
  };

  return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
}

// Component chỉ cần user
function UserProfile() {
  const { user } = useContext(AppContext);

  console.log('UserProfile rendered!');

  return <div>{user?.name}</div>;
}

// Component chỉ cần theme
function ThemeToggle() {
  const { theme, setTheme } = useContext(AppContext);

  console.log('ThemeToggle rendered!');

  return (
    <button onClick={() => setTheme((t) => (t === 'light' ? 'dark' : 'light'))}>
      {theme}
    </button>
  );
}

// ❌ VẤN ĐỀ:
// - Change theme → UserProfile re-render (KHÔNG cần!)
// - Change user → ThemeToggle re-render (KHÔNG cần!)
// - Mỗi setState → TẤT CẢ consumers re-render
// - Value object mới mỗi render → Context change detection

Root Causes:

  1. Value Object Recreation: {} mới mỗi render → Context "thay đổi"
  2. God Context: Tất cả state ở 1 context → Mọi change affect all
  3. No Memoization: Functions tạo mới mỗi render

1.2 Giải Pháp

3 Optimization Strategies:

jsx
/**
 * ✅ STRATEGY 1: Memoize Context Value
 */

function AppProvider({ children }) {
  const [user, setUser] = useState(null);
  const [theme, setTheme] = useState('light');

  // ✅ Memoize value object
  const value = useMemo(
    () => ({ user, setUser, theme, setTheme }),
    [user, theme], // Chỉ tạo mới khi dependencies thay đổi
  );

  return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
}

/**
 * ✅ STRATEGY 2: Context Splitting
 */

const UserContext = createContext();
const ThemeContext = createContext();

function UserProvider({ children }) {
  const [user, setUser] = useState(null);
  const value = useMemo(() => ({ user, setUser }), [user]);

  return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
}

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  const value = useMemo(() => ({ theme, setTheme }), [theme]);

  return (
    <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
  );
}

// ✅ Bây giờ: Change theme → KHÔNG affect UserProfile!

/**
 * ✅ STRATEGY 3: State + Dispatch Splitting
 */

const StateContext = createContext();
const DispatchContext = createContext();

function CartProvider({ children }) {
  const [state, dispatch] = useReducer(cartReducer, initialState);

  // State change nhiều, dispatch KHÔNG bao giờ change
  // → Components chỉ dùng dispatch KHÔNG re-render khi state change!

  return (
    <StateContext.Provider value={state}>
      <DispatchContext.Provider value={dispatch}>
        {children}
      </DispatchContext.Provider>
    </StateContext.Provider>
  );
}

function useCartState() {
  return useContext(StateContext);
}

function useCartDispatch() {
  return useContext(DispatchContext);
}

// Component chỉ dispatch (add to cart)
function AddToCartButton() {
  const dispatch = useCartDispatch(); // KHÔNG re-render khi cart items change!

  return <button onClick={() => dispatch({ type: 'ADD' })}>Add</button>;
}

1.3 Mental Model

CONTEXT PERFORMANCE:

Unoptimized:
Provider { value: NEW_OBJECT }  ← Mỗi render

Consumer A (dùng user) ← Re-render
Consumer B (dùng theme) ← Re-render
Consumer C (dùng notifications) ← Re-render
→ Tất cả re-render khi BẤT KỲ state nào change!

Optimized với useMemo:
Provider { value: SAME_OBJECT }  ← Nếu dependencies không đổi

Consumer A, B, C ← KHÔNG re-render nếu value không đổi

Optimized với Splitting:
UserProvider

Consumer A (user) ← Re-render khi user change

ThemeProvider

Consumer B (theme) ← Re-render khi theme change

→ Isolated re-renders!

Tương tự như: RADIO CHANNELS
- Single Context = 1 channel broadcast tất cả
  → Ai nghe channel này đều nhận TẤT CẢ updates
- Multiple Contexts = Nhiều channels riêng
  → Chỉ nghe channel cần thiết, bỏ qua các channel khác

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

"Luôn luôn optimize Context" → ✅ Premature optimization is EVIL! Chỉ optimize khi có performance issue THẬT

"useMemo giải quyết mọi vấn đề" → ✅ useMemo chỉ giải quyết object recreation. Vẫn cần split contexts cho best performance

"Context chậm, không dùng production" → ✅ Context OK cho production! Chỉ cần optimize đúng cách

"Optimization luôn tốt hơn" → ✅ Optimization có COST: Code phức tạp hơn, khó maintain. Trade-off!


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

Demo 1: Profiling Performance Issues ⭐

jsx
/**
 * 🎯 Detect performance issues với React DevTools
 * - Highlight updates
 * - Profiler
 * - Unnecessary re-renders
 */

import { createContext, useContext, useState } from 'react';

// ❌ UNOPTIMIZED VERSION
const AppContext = createContext();

function AppProvider({ children }) {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');

  // ❌ New object every render
  const value = {
    count,
    setCount,
    name,
    setName,
  };

  console.log('AppProvider rendered');

  return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
}

// Component chỉ dùng count
function Counter() {
  const { count, setCount } = useContext(AppContext);

  console.log('Counter rendered'); // Logs mỗi khi name change!

  return (
    <div style={{ padding: '20px', border: '2px solid blue', margin: '10px' }}>
      <h3>Counter Component</h3>
      <p>Count: {count}</p>
      <button onClick={() => setCount((c) => c + 1)}>Increment</button>
    </div>
  );
}

// Component chỉ dùng name
function NameInput() {
  const { name, setName } = useContext(AppContext);

  console.log('NameInput rendered'); // Logs mỗi khi count change!

  return (
    <div style={{ padding: '20px', border: '2px solid green', margin: '10px' }}>
      <h3>Name Input Component</h3>
      <input
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder='Enter name'
        style={{ padding: '5px' }}
      />
      <p>Name: {name}</p>
    </div>
  );
}

// Component KHÔNG dùng context
function StaticComponent() {
  console.log('StaticComponent rendered'); // Vẫn re-render!

  return (
    <div style={{ padding: '20px', border: '2px solid red', margin: '10px' }}>
      <h3>Static Component</h3>
      <p>I don't use any context value!</p>
    </div>
  );
}

function App() {
  return (
    <AppProvider>
      <div style={{ maxWidth: '600px', margin: '20px auto' }}>
        <h1>Performance Issue Demo</h1>

        <div
          style={{
            padding: '10px',
            background: '#fff3cd',
            borderRadius: '4px',
            marginBottom: '20px',
          }}
        >
          <strong>🔍 Open Console & React DevTools Profiler</strong>
          <ol>
            <li>Click Increment → Counter, NameInput, Static ALL re-render</li>
            <li>Type in Name → Counter, NameInput, Static ALL re-render</li>
            <li>Static component re-renders despite NOT using context!</li>
          </ol>
        </div>

        <Counter />
        <NameInput />
        <StaticComponent />
      </div>
    </AppProvider>
  );
}

/**
 * PROFILING STEPS:
 *
 * 1. Open React DevTools → Profiler tab
 * 2. Click "Record"
 * 3. Increment counter 3 times
 * 4. Stop recording
 * 5. See flamegraph: Counter, NameInput, StaticComponent all rendered
 *
 * 6. Enable "Highlight updates" in React DevTools settings
 * 7. Type in name input
 * 8. See ALL components flash (re-render)
 *
 * CONCLUSION:
 * - Context change → ALL children re-render (even if not using context)
 * - Need optimization!
 */

Demo 2: Optimization với useMemo ⭐⭐

jsx
/**
 * 🎯 Fix performance với useMemo
 * - Memoize context value
 * - React.memo cho components
 */

import { createContext, useContext, useState, useMemo } from 'react';
import { memo } from 'react';

// ✅ OPTIMIZED VERSION
const AppContext = createContext();

function AppProvider({ children }) {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');

  // ✅ Memoize value object
  const value = useMemo(
    () => ({
      count,
      setCount,
      name,
      setName,
    }),
    [count, name], // Only recreate when these change
  );

  console.log('AppProvider rendered');

  return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
}

// ✅ Wrap với React.memo
const Counter = memo(function Counter() {
  const { count, setCount } = useContext(AppContext);

  console.log('Counter rendered');

  return (
    <div style={{ padding: '20px', border: '2px solid blue', margin: '10px' }}>
      <h3>Counter Component (Memoized)</h3>
      <p>Count: {count}</p>
      <button onClick={() => setCount((c) => c + 1)}>Increment</button>
    </div>
  );
});

const NameInput = memo(function NameInput() {
  const { name, setName } = useContext(AppContext);

  console.log('NameInput rendered');

  return (
    <div style={{ padding: '20px', border: '2px solid green', margin: '10px' }}>
      <h3>Name Input Component (Memoized)</h3>
      <input
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder='Enter name'
        style={{ padding: '5px' }}
      />
      <p>Name: {name}</p>
    </div>
  );
});

// ✅ Memo và KHÔNG dùng context
const StaticComponent = memo(function StaticComponent() {
  console.log('StaticComponent rendered');

  return (
    <div style={{ padding: '20px', border: '2px solid red', margin: '10px' }}>
      <h3>Static Component (Memoized)</h3>
      <p>I don't use any context value!</p>
      <p>I should NOT re-render now!</p>
    </div>
  );
});

function App() {
  return (
    <AppProvider>
      <div style={{ maxWidth: '600px', margin: '20px auto' }}>
        <h1>Optimized with useMemo + React.memo</h1>

        <div
          style={{
            padding: '10px',
            background: '#d4edda',
            borderRadius: '4px',
            marginBottom: '20px',
          }}
        >
          <strong>✅ Improvements:</strong>
          <ol>
            <li>Click Increment → Only Counter re-renders</li>
            <li>Type in Name → Only NameInput re-renders</li>
            <li>Static component NEVER re-renders</li>
          </ol>
        </div>

        <Counter />
        <NameInput />
        <StaticComponent />
      </div>
    </AppProvider>
  );
}

/**
 * WHY IT WORKS:
 *
 * useMemo:
 * - Value object chỉ tạo mới khi count hoặc name thay đổi
 * - Same object reference → Context không "change"
 *
 * React.memo:
 * - Component chỉ re-render khi props thay đổi
 * - Context value là "prop" hidden
 * - Value không đổi → Component không re-render
 *
 * LIMITATION:
 * - Vẫn có issue: Counter re-renders khi name changes (do context value change)
 * - NameInput re-renders khi count changes
 * - Need Context Splitting cho perfect optimization!
 */

Demo 3: Context Splitting Pattern ⭐⭐⭐

jsx
/**
 * 🎯 Perfect optimization: Split contexts
 * - CountContext
 * - NameContext
 * - Isolated re-renders
 */

import { createContext, useContext, useState, useMemo, memo } from 'react';

// ✅ SPLIT CONTEXTS
const CountContext = createContext();
const NameContext = createContext();

function CountProvider({ children }) {
  const [count, setCount] = useState(0);

  const value = useMemo(() => ({ count, setCount }), [count]);

  console.log('CountProvider rendered');

  return (
    <CountContext.Provider value={value}>{children}</CountContext.Provider>
  );
}

function NameProvider({ children }) {
  const [name, setName] = useState('');

  const value = useMemo(() => ({ name, setName }), [name]);

  console.log('NameProvider rendered');

  return <NameContext.Provider value={value}>{children}</NameContext.Provider>;
}

// Custom hooks
function useCount() {
  const context = useContext(CountContext);
  if (!context) throw new Error('useCount must be used within CountProvider');
  return context;
}

function useName() {
  const context = useContext(NameContext);
  if (!context) throw new Error('useName must be used within NameProvider');
  return context;
}

// Components
const Counter = memo(function Counter() {
  const { count, setCount } = useCount();

  console.log('Counter rendered');

  return (
    <div style={{ padding: '20px', border: '2px solid blue', margin: '10px' }}>
      <h3>Counter Component</h3>
      <p>Count: {count}</p>
      <button onClick={() => setCount((c) => c + 1)}>Increment</button>
      <p style={{ fontSize: '12px', color: '#666' }}>
        ✅ Only re-renders when count changes
      </p>
    </div>
  );
});

const NameInput = memo(function NameInput() {
  const { name, setName } = useName();

  console.log('NameInput rendered');

  return (
    <div style={{ padding: '20px', border: '2px solid green', margin: '10px' }}>
      <h3>Name Input Component</h3>
      <input
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder='Enter name'
        style={{ padding: '5px' }}
      />
      <p>Name: {name}</p>
      <p style={{ fontSize: '12px', color: '#666' }}>
        ✅ Only re-renders when name changes
      </p>
    </div>
  );
});

// Component dùng CẢ 2 contexts
const Summary = memo(function Summary() {
  const { count } = useCount();
  const { name } = useName();

  console.log('Summary rendered');

  return (
    <div
      style={{ padding: '20px', border: '2px solid purple', margin: '10px' }}
    >
      <h3>Summary Component</h3>
      <p>Count: {count}</p>
      <p>Name: {name || '(empty)'}</p>
      <p style={{ fontSize: '12px', color: '#666' }}>
        ⚠️ Re-renders when EITHER count or name changes
      </p>
    </div>
  );
});

const StaticComponent = memo(function StaticComponent() {
  console.log('StaticComponent rendered');

  return (
    <div style={{ padding: '20px', border: '2px solid red', margin: '10px' }}>
      <h3>Static Component</h3>
      <p>I don't use any context!</p>
      <p style={{ fontSize: '12px', color: '#666' }}>✅ NEVER re-renders</p>
    </div>
  );
});

function App() {
  return (
    <CountProvider>
      <NameProvider>
        <div style={{ maxWidth: '600px', margin: '20px auto' }}>
          <h1>Perfect Optimization: Context Splitting</h1>

          <div
            style={{
              padding: '10px',
              background: '#d1ecf1',
              borderRadius: '4px',
              marginBottom: '20px',
            }}
          >
            <strong>🎯 Perfect Isolation:</strong>
            <ol>
              <li>Increment → ONLY Counter + Summary re-render</li>
              <li>Type name → ONLY NameInput + Summary re-render</li>
              <li>Static → NEVER re-renders</li>
            </ol>
          </div>

          <Counter />
          <NameInput />
          <Summary />
          <StaticComponent />
        </div>
      </NameProvider>
    </CountProvider>
  );
}

/**
 * PERFECT OPTIMIZATION ACHIEVED:
 *
 * Before (Single Context):
 * - Change count → Counter, NameInput, Summary, Static all re-render
 * - Change name → Counter, NameInput, Summary, Static all re-render
 *
 * After (Split Contexts):
 * - Change count → Counter, Summary re-render (ONLY!)
 * - Change name → NameInput, Summary re-render (ONLY!)
 * - Static → NEVER re-renders
 *
 * TRADE-OFFS:
 * ✅ Perfect performance
 * ✅ Isolated re-renders
 * ❌ More boilerplate (2 providers instead of 1)
 * ❌ Provider nesting
 *
 * WHEN TO USE:
 * - Large apps với nhiều independent state
 * - Performance-critical apps
 * - State thay đổi frequently
 */

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

⭐ Level 1: useMemo Context Value (15 phút)

jsx
/**
 * 🎯 Mục tiêu: Optimize context value với useMemo
 * ⏱️ Thời gian: 15 phút
 * 🚫 KHÔNG dùng: Context splitting (chỉ useMemo)
 *
 * Requirements:
 * 1. TodoContext với todos[], filter
 * 2. useMemo cho value object
 * 3. useCallback cho add/remove/toggle functions
 * 4. TodoList component với React.memo
 * 5. FilterButtons component với React.memo
 *
 * 💡 Gợi ý:
 * - Dependencies của useMemo: [todos, filter]
 * - useCallback dependencies: [] (dùng functional updates)
 */

// ❌ Cách SAI:
// - Không dùng useMemo cho value
// - Không dùng useCallback cho functions
// - Không wrap components với React.memo

// ✅ Cách ĐÚNG: Xem solution

// 🎯 NHIỆM VỤ:
// TODO: Implement TodoProvider với useMemo
// TODO: Implement addTodo, removeTodo với useCallback
// TODO: TodoList với React.memo
// TODO: FilterButtons với React.memo
💡 Solution
jsx
import {
  createContext,
  useContext,
  useState,
  useMemo,
  useCallback,
  memo,
} from 'react';

/**
 * Todo Context với useMemo optimization
 */

const TodoContext = createContext();

function TodoProvider({ children }) {
  const [todos, setTodos] = useState([]);
  const [filter, setFilter] = useState('all'); // 'all' | 'active' | 'completed'

  // ✅ useCallback cho functions (dependencies [])
  const addTodo = useCallback((text) => {
    setTodos((prev) => [...prev, { id: Date.now(), text, completed: false }]);
  }, []); // Empty deps vì dùng functional update

  const removeTodo = useCallback((id) => {
    setTodos((prev) => prev.filter((todo) => todo.id !== id));
  }, []);

  const toggleTodo = useCallback((id) => {
    setTodos((prev) =>
      prev.map((todo) =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo,
      ),
    );
  }, []);

  // ✅ useMemo cho value object
  const value = useMemo(
    () => ({
      todos,
      filter,
      setFilter,
      addTodo,
      removeTodo,
      toggleTodo,
    }),
    [todos, filter, addTodo, removeTodo, toggleTodo],
    // Note: addTodo, removeTodo, toggleTodo stable (useCallback)
  );

  console.log('TodoProvider rendered');

  return <TodoContext.Provider value={value}>{children}</TodoContext.Provider>;
}

function useTodos() {
  const context = useContext(TodoContext);
  if (!context) throw new Error('useTodos must be used within TodoProvider');
  return context;
}

// ✅ React.memo components
const TodoInput = memo(function TodoInput() {
  const { addTodo } = useTodos();
  const [text, setText] = useState('');

  console.log('TodoInput rendered');

  const handleSubmit = (e) => {
    e.preventDefault();
    if (text.trim()) {
      addTodo(text);
      setText('');
    }
  };

  return (
    <form
      onSubmit={handleSubmit}
      style={{ marginBottom: '20px' }}
    >
      <input
        value={text}
        onChange={(e) => setText(e.target.value)}
        placeholder='Add todo...'
        style={{ padding: '8px', width: '300px' }}
      />
      <button
        type='submit'
        style={{ marginLeft: '10px', padding: '8px' }}
      >
        Add
      </button>
    </form>
  );
});

const TodoItem = memo(function TodoItem({ todo, onToggle, onRemove }) {
  console.log(`TodoItem ${todo.id} rendered`);

  return (
    <div
      style={{
        display: 'flex',
        alignItems: 'center',
        padding: '8px',
        borderBottom: '1px solid #eee',
      }}
    >
      <input
        type='checkbox'
        checked={todo.completed}
        onChange={() => onToggle(todo.id)}
      />
      <span
        style={{
          flex: 1,
          marginLeft: '10px',
          textDecoration: todo.completed ? 'line-through' : 'none',
        }}
      >
        {todo.text}
      </span>
      <button onClick={() => onRemove(todo.id)}>Delete</button>
    </div>
  );
});

const TodoList = memo(function TodoList() {
  const { todos, filter, toggleTodo, removeTodo } = useTodos();

  console.log('TodoList rendered');

  const filteredTodos = todos.filter((todo) => {
    if (filter === 'active') return !todo.completed;
    if (filter === 'completed') return todo.completed;
    return true;
  });

  if (filteredTodos.length === 0) {
    return <p>No todos!</p>;
  }

  return (
    <div>
      {filteredTodos.map((todo) => (
        <TodoItem
          key={todo.id}
          todo={todo}
          onToggle={toggleTodo}
          onRemove={removeTodo}
        />
      ))}
    </div>
  );
});

const FilterButtons = memo(function FilterButtons() {
  const { filter, setFilter } = useTodos();

  console.log('FilterButtons rendered');

  const filters = ['all', 'active', 'completed'];

  return (
    <div style={{ marginTop: '20px' }}>
      {filters.map((f) => (
        <button
          key={f}
          onClick={() => setFilter(f)}
          style={{
            marginRight: '10px',
            padding: '5px 15px',
            background: filter === f ? '#007bff' : '#fff',
            color: filter === f ? '#fff' : '#000',
            border: '1px solid #007bff',
            cursor: 'pointer',
          }}
        >
          {f.charAt(0).toUpperCase() + f.slice(1)}
        </button>
      ))}
    </div>
  );
});

function App() {
  return (
    <TodoProvider>
      <div style={{ maxWidth: '500px', margin: '20px auto' }}>
        <h1>Optimized Todo App</h1>

        <div
          style={{
            padding: '10px',
            background: '#e3f2fd',
            borderRadius: '4px',
            marginBottom: '20px',
            fontSize: '14px',
          }}
        >
          <strong>✅ Optimizations:</strong>
          <ul>
            <li>useMemo for context value</li>
            <li>useCallback for functions</li>
            <li>React.memo for components</li>
            <li>Check console to see re-renders!</li>
          </ul>
        </div>

        <TodoInput />
        <TodoList />
        <FilterButtons />
      </div>
    </TodoProvider>
  );
}

/**
 * Result:
 * - Add todo → TodoList re-renders, FilterButtons KHÔNG
 * - Change filter → TodoList re-renders (filter), FilterButtons re-renders
 * - Toggle todo → Chỉ TodoItem đó re-render (nếu optimize thêm)
 */

⭐⭐ Level 2: State + Dispatch Splitting (25 phút)

jsx
/**
 * 🎯 Mục tiêu: Optimize với State/Dispatch splitting
 * ⏱️ Thời gian: 25 phút
 *
 * Scenario: Shopping Cart với nhiều operations
 * Problem: Add to cart button re-render mỗi khi cart items change
 *
 * Solution: Split StateContext và DispatchContext
 *
 * Requirements:
 * 1. CartStateContext - chứa cart state
 * 2. CartDispatchContext - chứa dispatch function
 * 3. useCartState() và useCartDispatch() hooks
 * 4. AddToCartButton chỉ dùng dispatch
 * 5. CartSummary chỉ dùng state
 * 6. Verify: AddToCartButton KHÔNG re-render khi items change
 */
💡 Solution
jsx
import { createContext, useContext, useReducer, memo } from 'react';

/**
 * Cart với State/Dispatch Splitting
 * Dispatch NEVER changes → Components dùng dispatch KHÔNG re-render
 */

// 1. Separate Contexts
const CartStateContext = createContext();
const CartDispatchContext = createContext();

// 2. Reducer
const cartReducer = (state, action) => {
  switch (action.type) {
    case 'ADD_ITEM': {
      const existing = state.items.find(
        (item) => item.id === action.payload.id,
      );

      if (existing) {
        return {
          ...state,
          items: state.items.map((item) =>
            item.id === action.payload.id
              ? { ...item, quantity: item.quantity + 1 }
              : item,
          ),
        };
      }

      return {
        ...state,
        items: [...state.items, { ...action.payload, quantity: 1 }],
      };
    }

    case 'REMOVE_ITEM':
      return {
        ...state,
        items: state.items.filter((item) => item.id !== action.payload),
      };

    case 'CLEAR':
      return { ...state, items: [] };

    default:
      return state;
  }
};

// 3. Provider
function CartProvider({ children }) {
  const [state, dispatch] = useReducer(cartReducer, { items: [] });

  // dispatch NEVER changes (stable reference)
  // state changes frequently

  return (
    <CartStateContext.Provider value={state}>
      <CartDispatchContext.Provider value={dispatch}>
        {children}
      </CartDispatchContext.Provider>
    </CartStateContext.Provider>
  );
}

// 4. Custom hooks
function useCartState() {
  const context = useContext(CartStateContext);
  if (!context)
    throw new Error('useCartState must be used within CartProvider');
  return context;
}

function useCartDispatch() {
  const context = useContext(CartDispatchContext);
  if (!context)
    throw new Error('useCartDispatch must be used within CartProvider');
  return context;
}

// 5. Components
const ProductCard = memo(function ProductCard({ product }) {
  // ✅ Chỉ dùng dispatch → KHÔNG re-render khi cart changes!
  const dispatch = useCartDispatch();

  console.log(`ProductCard ${product.id} rendered`);

  return (
    <div
      style={{
        border: '1px solid #ddd',
        padding: '15px',
        margin: '10px',
        borderRadius: '4px',
      }}
    >
      <h3>{product.name}</h3>
      <p>${product.price}</p>
      <button
        onClick={() => dispatch({ type: 'ADD_ITEM', payload: product })}
        style={{ padding: '8px 16px' }}
      >
        Add to Cart
      </button>
      <p style={{ fontSize: '12px', color: '#666' }}>
        ✅ This button doesn't re-render when cart changes!
      </p>
    </div>
  );
});

const CartSummary = memo(function CartSummary() {
  // ✅ Chỉ dùng state → Re-render khi cart changes (expected!)
  const state = useCartState();
  const dispatch = useCartDispatch();

  console.log('CartSummary rendered');

  const totalItems = state.items.reduce((sum, item) => sum + item.quantity, 0);
  const totalPrice = state.items.reduce(
    (sum, item) => sum + item.price * item.quantity,
    0,
  );

  if (state.items.length === 0) {
    return (
      <div
        style={{ padding: '20px', background: '#f5f5f5', borderRadius: '4px' }}
      >
        <p>Cart is empty</p>
      </div>
    );
  }

  return (
    <div
      style={{ padding: '20px', background: '#f5f5f5', borderRadius: '4px' }}
    >
      <h3>Cart Summary</h3>

      {state.items.map((item) => (
        <div
          key={item.id}
          style={{
            display: 'flex',
            justifyContent: 'space-between',
            padding: '5px 0',
            borderBottom: '1px solid #ddd',
          }}
        >
          <span>
            {item.name} x{item.quantity}
          </span>
          <span>${item.price * item.quantity}</span>
          <button
            onClick={() => dispatch({ type: 'REMOVE_ITEM', payload: item.id })}
            style={{ marginLeft: '10px' }}
          >
            Remove
          </button>
        </div>
      ))}

      <div style={{ marginTop: '15px', fontWeight: 'bold' }}>
        <div>Total Items: {totalItems}</div>
        <div>Total Price: ${totalPrice}</div>
      </div>

      <button
        onClick={() => dispatch({ type: 'CLEAR' })}
        style={{
          marginTop: '10px',
          padding: '8px 16px',
          background: '#f44336',
          color: 'white',
          border: 'none',
          borderRadius: '4px',
          cursor: 'pointer',
        }}
      >
        Clear Cart
      </button>

      <p style={{ fontSize: '12px', color: '#666', marginTop: '10px' }}>
        ⚠️ This component re-renders when cart changes (expected!)
      </p>
    </div>
  );
});

function App() {
  const products = [
    { id: 1, name: 'Laptop', price: 999 },
    { id: 2, name: 'Mouse', price: 29 },
    { id: 3, name: 'Keyboard', price: 79 },
  ];

  return (
    <CartProvider>
      <div style={{ maxWidth: '800px', margin: '20px auto' }}>
        <h1>State/Dispatch Splitting Pattern</h1>

        <div
          style={{
            padding: '15px',
            background: '#d1ecf1',
            borderRadius: '4px',
            marginBottom: '20px',
          }}
        >
          <strong>🎯 Optimization:</strong>
          <ul>
            <li>
              ProductCard buttons: Use dispatch only → NO re-render on cart
              change
            </li>
            <li>
              CartSummary: Uses state → Re-renders on cart change (expected)
            </li>
            <li>Open console to verify!</li>
          </ul>
        </div>

        <h2>Products</h2>
        <div style={{ display: 'flex', flexWrap: 'wrap' }}>
          {products.map((product) => (
            <ProductCard
              key={product.id}
              product={product}
            />
          ))}
        </div>

        <h2>Cart</h2>
        <CartSummary />
      </div>
    </CartProvider>
  );
}

/**
 * WHY IT WORKS:
 *
 * dispatch reference:
 * - Created once by useReducer
 * - NEVER changes
 * - Stable across re-renders
 *
 * Components using dispatch only:
 * - Get same dispatch reference every time
 * - No props change → No re-render (với React.memo)
 *
 * Components using state:
 * - State changes → Context value changes → Re-render
 * - This is EXPECTED and CORRECT!
 *
 * PATTERN:
 * - "Write-only" components → useCartDispatch()
 * - "Read-only" components → useCartState()
 * - "Read-write" components → Both hooks
 */

⭐⭐⭐ Level 3: Simple Selector Pattern (40 phút)

jsx
/**
 * 🎯 Mục tiêu: Implement selector pattern
 * ⏱️ Thời gian: 40 phút
 *
 * 📋 Product Requirements:
 * User Story: "Là developer, tôi muốn components chỉ subscribe
 *              vào phần state chúng cần để tránh re-render không cần thiết"
 *
 * ✅ Acceptance Criteria:
 * - [ ] useSelector(selector) hook
 * - [ ] Components chỉ re-render khi selected value thay đổi
 * - [ ] Selector function: (state) => value
 * - [ ] Shallow comparison cho selected value
 * - [ ] Demo với user state: { name, email, age, preferences }
 *
 * 🎨 Technical Constraints:
 * - KHÔNG dùng external libraries
 * - Tự implement với useRef và useEffect
 * - Simple shallow comparison (===)
 *
 * 📝 Implementation Checklist:
 * - [ ] Context với complex state
 * - [ ] useSelector hook implementation
 * - [ ] Components dùng selectors
 * - [ ] Verify isolated re-renders
 */
💡 Solution
jsx
import {
  createContext,
  useContext,
  useState,
  useRef,
  useEffect,
  useSyncExternalStore,
} from 'react';

/**
 * Simple Selector Pattern
 * Components subscribe to specific slices of state
 */

const UserContext = createContext();

function UserProvider({ children }) {
  const [state, setState] = useState({
    name: 'John Doe',
    email: 'john@example.com',
    age: 30,
    preferences: {
      theme: 'light',
      language: 'en',
      notifications: true,
    },
  });

  // Listeners for useSyncExternalStore
  const listenersRef = useRef(new Set());

  const subscribe = (callback) => {
    listenersRef.current.add(callback);
    return () => {
      listenersRef.current.delete(callback);
    };
  };

  const updateState = (updater) => {
    setState((prev) => {
      const next = typeof updater === 'function' ? updater(prev) : updater;

      // Notify listeners
      listenersRef.current.forEach((callback) => callback());

      return next;
    });
  };

  const value = {
    state,
    updateState,
    subscribe,
  };

  return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
}

// ✅ useSelector implementation
function useUserSelector(selector) {
  const { state, subscribe } = useContext(UserContext);

  // Use React's built-in useSyncExternalStore for proper sync
  const selectedValue = useSyncExternalStore(
    subscribe,
    () => selector(state),
    () => selector(state),
  );

  return selectedValue;
}

function useUserUpdate() {
  const { updateState } = useContext(UserContext);
  return updateState;
}

// Components với selectors
function NameDisplay() {
  // ✅ Only subscribe to name
  const name = useUserSelector((state) => state.name);

  console.log('NameDisplay rendered');

  return (
    <div style={{ padding: '15px', border: '2px solid blue', margin: '10px' }}>
      <h3>Name Display</h3>
      <p>Name: {name}</p>
      <p style={{ fontSize: '12px', color: '#666' }}>
        ✅ Only re-renders when name changes
      </p>
    </div>
  );
}

function EmailDisplay() {
  // ✅ Only subscribe to email
  const email = useUserSelector((state) => state.email);

  console.log('EmailDisplay rendered');

  return (
    <div style={{ padding: '15px', border: '2px solid green', margin: '10px' }}>
      <h3>Email Display</h3>
      <p>Email: {email}</p>
      <p style={{ fontSize: '12px', color: '#666' }}>
        ✅ Only re-renders when email changes
      </p>
    </div>
  );
}

function AgeDisplay() {
  // ✅ Only subscribe to age
  const age = useUserSelector((state) => state.age);

  console.log('AgeDisplay rendered');

  return (
    <div
      style={{ padding: '15px', border: '2px solid orange', margin: '10px' }}
    >
      <h3>Age Display</h3>
      <p>Age: {age}</p>
      <p style={{ fontSize: '12px', color: '#666' }}>
        ✅ Only re-renders when age changes
      </p>
    </div>
  );
}

function ThemeDisplay() {
  // ✅ Only subscribe to theme (nested)
  const theme = useUserSelector((state) => state.preferences.theme);

  console.log('ThemeDisplay rendered');

  return (
    <div
      style={{ padding: '15px', border: '2px solid purple', margin: '10px' }}
    >
      <h3>Theme Display</h3>
      <p>Theme: {theme}</p>
      <p style={{ fontSize: '12px', color: '#666' }}>
        ✅ Only re-renders when theme changes
      </p>
    </div>
  );
}

function Controls() {
  const updateState = useUserUpdate();

  console.log('Controls rendered');

  return (
    <div
      style={{
        padding: '20px',
        background: '#f5f5f5',
        borderRadius: '4px',
        marginBottom: '20px',
      }}
    >
      <h3>Controls</h3>

      <div style={{ marginBottom: '10px' }}>
        <button
          onClick={() =>
            updateState((prev) => ({
              ...prev,
              name: 'Jane Doe',
            }))
          }
          style={{ marginRight: '10px', padding: '5px 10px' }}
        >
          Change Name
        </button>

        <button
          onClick={() =>
            updateState((prev) => ({
              ...prev,
              email: 'jane@example.com',
            }))
          }
          style={{ marginRight: '10px', padding: '5px 10px' }}
        >
          Change Email
        </button>

        <button
          onClick={() =>
            updateState((prev) => ({
              ...prev,
              age: prev.age + 1,
            }))
          }
          style={{ marginRight: '10px', padding: '5px 10px' }}
        >
          Increment Age
        </button>

        <button
          onClick={() =>
            updateState((prev) => ({
              ...prev,
              preferences: {
                ...prev.preferences,
                theme: prev.preferences.theme === 'light' ? 'dark' : 'light',
              },
            }))
          }
          style={{ padding: '5px 10px' }}
        >
          Toggle Theme
        </button>
      </div>

      <p style={{ fontSize: '12px', color: '#666', marginTop: '10px' }}>
        ⚠️ This component always re-renders (has buttons)
      </p>
    </div>
  );
}

function App() {
  return (
    <UserProvider>
      <div style={{ maxWidth: '800px', margin: '20px auto' }}>
        <h1>Selector Pattern Demo</h1>

        <div
          style={{
            padding: '15px',
            background: '#d4edda',
            borderRadius: '4px',
            marginBottom: '20px',
          }}
        >
          <strong>🎯 Perfect Isolation:</strong>
          <ul>
            <li>Change Name → ONLY NameDisplay re-renders</li>
            <li>Change Email → ONLY EmailDisplay re-renders</li>
            <li>Change Age → ONLY AgeDisplay re-renders</li>
            <li>Toggle Theme → ONLY ThemeDisplay re-renders</li>
            <li>Open console to verify!</li>
          </ul>
        </div>

        <Controls />

        <div
          style={{
            display: 'grid',
            gridTemplateColumns: '1fr 1fr',
            gap: '10px',
          }}
        >
          <NameDisplay />
          <EmailDisplay />
          <AgeDisplay />
          <ThemeDisplay />
        </div>
      </div>
    </UserProvider>
  );
}

/**
 * HOW IT WORKS:
 *
 * useSyncExternalStore:
 * - React 18 hook cho external stores
 * - subscribe: Function to subscribe to store
 * - getSnapshot: Function to get current value
 * - Automatically re-renders when snapshot changes
 *
 * useUserSelector:
 * - Takes selector function: (state) => value
 * - Returns selected value
 * - Only re-renders when selected value changes (shallow comparison)
 *
 * BENEFITS:
 * ✅ Perfect isolation - components only re-render when needed
 * ✅ Flexible selectors - can select any slice of state
 * ✅ No library needed - uses React built-ins
 *
 * LIMITATIONS:
 * - Shallow comparison only (=== check)
 * - For deep comparison, need custom implementation
 * - More complex than simple Context
 */

⭐⭐⭐⭐ Level 4: Optimized Todo App (60 phút)

jsx
/**
 * 🎯 Mục tiêu: Apply tất cả optimization patterns
 * ⏱️ Thời gian: 60 phút
 *
 * 🏗️ PHASE 1: Unoptimized Version (15 phút)
 * - Single TodoContext
 * - No memoization
 * - Measure performance issues
 *
 * 🏗️ PHASE 2: Add Optimizations (30 phút)
 * - Split State/Dispatch contexts
 * - useMemo/useCallback
 * - React.memo components
 * - Selector pattern (optional)
 *
 * 🧪 PHASE 3: Performance Comparison (15 phút)
 * - Profile both versions
 * - Document improvements
 * - Measure re-render count
 *
 * Requirements:
 * - 100+ todos để test performance
 * - Filter, sort, search features
 * - Add, edit, delete, toggle operations
 * - Console logs để track re-renders
 */
💡 Solution
jsx
import {
  createContext,
  useContext,
  useReducer,
  useMemo,
  useCallback,
  memo,
} from 'react';

/**
 * OPTIMIZED TODO APP
 * Applies all optimization patterns learned
 */

// ========================================
// CONTEXTS (Split State/Dispatch)
// ========================================

const TodoStateContext = createContext();
const TodoDispatchContext = createContext();

// ========================================
// REDUCER
// ========================================

const todoReducer = (state, action) => {
  switch (action.type) {
    case 'ADD_TODO':
      return {
        ...state,
        todos: [
          ...state.todos,
          {
            id: Date.now(),
            text: action.payload,
            completed: false,
            createdAt: new Date().toISOString(),
          },
        ],
      };

    case 'TOGGLE_TODO':
      return {
        ...state,
        todos: state.todos.map((todo) =>
          todo.id === action.payload
            ? { ...todo, completed: !todo.completed }
            : todo,
        ),
      };

    case 'DELETE_TODO':
      return {
        ...state,
        todos: state.todos.filter((todo) => todo.id !== action.payload),
      };

    case 'SET_FILTER':
      return {
        ...state,
        filter: action.payload,
      };

    case 'SET_SEARCH':
      return {
        ...state,
        searchQuery: action.payload,
      };

    case 'BULK_ADD':
      return {
        ...state,
        todos: [...state.todos, ...action.payload],
      };

    default:
      return state;
  }
};

// ========================================
// PROVIDER
// ========================================

function TodoProvider({ children }) {
  const [state, dispatch] = useReducer(todoReducer, {
    todos: [],
    filter: 'all',
    searchQuery: '',
  });

  console.log('TodoProvider rendered');

  return (
    <TodoStateContext.Provider value={state}>
      <TodoDispatchContext.Provider value={dispatch}>
        {children}
      </TodoDispatchContext.Provider>
    </TodoStateContext.Provider>
  );
}

// ========================================
// HOOKS
// ========================================

function useTodoState() {
  const context = useContext(TodoStateContext);
  if (!context)
    throw new Error('useTodoState must be used within TodoProvider');
  return context;
}

function useTodoDispatch() {
  const context = useContext(TodoDispatchContext);
  if (!context)
    throw new Error('useTodoDispatch must be used within TodoProvider');
  return context;
}

// ✅ Memoized selector hooks
function useFilteredTodos() {
  const { todos, filter, searchQuery } = useTodoState();

  return useMemo(() => {
    let filtered = todos;

    // Apply filter
    if (filter === 'active') {
      filtered = filtered.filter((t) => !t.completed);
    } else if (filter === 'completed') {
      filtered = filtered.filter((t) => t.completed);
    }

    // Apply search
    if (searchQuery) {
      filtered = filtered.filter((t) =>
        t.text.toLowerCase().includes(searchQuery.toLowerCase()),
      );
    }

    return filtered;
  }, [todos, filter, searchQuery]);
}

function useTodoStats() {
  const { todos } = useTodoState();

  return useMemo(
    () => ({
      total: todos.length,
      active: todos.filter((t) => !t.completed).length,
      completed: todos.filter((t) => t.completed).length,
    }),
    [todos],
  );
}

// ========================================
// COMPONENTS
// ========================================

// ✅ Only uses dispatch - NEVER re-renders
const TodoInput = memo(function TodoInput() {
  const dispatch = useTodoDispatch();
  const [text, setText] = useState('');

  console.log('TodoInput rendered');

  const handleSubmit = (e) => {
    e.preventDefault();
    if (text.trim()) {
      dispatch({ type: 'ADD_TODO', payload: text });
      setText('');
    }
  };

  return (
    <form
      onSubmit={handleSubmit}
      style={{ marginBottom: '20px' }}
    >
      <input
        value={text}
        onChange={(e) => setText(e.target.value)}
        placeholder='Add todo...'
        style={{ padding: '10px', width: '400px', fontSize: '16px' }}
      />
      <button
        type='submit'
        style={{ marginLeft: '10px', padding: '10px 20px', fontSize: '16px' }}
      >
        Add
      </button>
      <p style={{ fontSize: '12px', color: '#666', marginTop: '5px' }}>
        ✅ Never re-renders (uses dispatch only)
      </p>
    </form>
  );
});

const useState = require('react').useState;

// ✅ Only uses dispatch - NEVER re-renders (except own state)
const SearchBar = memo(function SearchBar() {
  const dispatch = useTodoDispatch();
  const { searchQuery } = useTodoState();

  console.log('SearchBar rendered');

  return (
    <div style={{ marginBottom: '20px' }}>
      <input
        value={searchQuery}
        onChange={(e) =>
          dispatch({ type: 'SET_SEARCH', payload: e.target.value })
        }
        placeholder='Search todos...'
        style={{ padding: '10px', width: '400px', fontSize: '16px' }}
      />
      <p style={{ fontSize: '12px', color: '#666', marginTop: '5px' }}>
        ⚠️ Re-renders when search query changes
      </p>
    </div>
  );
});

// ✅ Only uses dispatch - NEVER re-renders
const FilterButtons = memo(function FilterButtons() {
  const dispatch = useTodoDispatch();
  const { filter } = useTodoState();

  console.log('FilterButtons rendered');

  const filters = ['all', 'active', 'completed'];

  return (
    <div style={{ marginBottom: '20px' }}>
      {filters.map((f) => (
        <button
          key={f}
          onClick={() => dispatch({ type: 'SET_FILTER', payload: f })}
          style={{
            marginRight: '10px',
            padding: '8px 16px',
            background: filter === f ? '#007bff' : '#fff',
            color: filter === f ? '#fff' : '#000',
            border: '1px solid #007bff',
            cursor: 'pointer',
            fontSize: '14px',
          }}
        >
          {f.charAt(0).toUpperCase() + f.slice(1)}
        </button>
      ))}
      <p style={{ fontSize: '12px', color: '#666', marginTop: '5px' }}>
        ⚠️ Re-renders when filter changes
      </p>
    </div>
  );
});

// ✅ Memoized, only re-renders when todo changes
const TodoItem = memo(
  function TodoItem({ todo }) {
    const dispatch = useTodoDispatch();

    console.log(`TodoItem ${todo.id} rendered`);

    return (
      <div
        style={{
          display: 'flex',
          alignItems: 'center',
          padding: '12px',
          borderBottom: '1px solid #eee',
          background: '#fff',
        }}
      >
        <input
          type='checkbox'
          checked={todo.completed}
          onChange={() => dispatch({ type: 'TOGGLE_TODO', payload: todo.id })}
          style={{ marginRight: '10px' }}
        />
        <span
          style={{
            flex: 1,
            textDecoration: todo.completed ? 'line-through' : 'none',
            color: todo.completed ? '#999' : '#000',
          }}
        >
          {todo.text}
        </span>
        <button
          onClick={() => dispatch({ type: 'DELETE_TODO', payload: todo.id })}
          style={{
            padding: '5px 10px',
            background: '#f44336',
            color: 'white',
            border: 'none',
            borderRadius: '4px',
            cursor: 'pointer',
          }}
        >
          Delete
        </button>
      </div>
    );
  },
  (prevProps, nextProps) => {
    // Custom comparison - only re-render if todo actually changed
    return (
      prevProps.todo.id === nextProps.todo.id &&
      prevProps.todo.completed === nextProps.todo.completed &&
      prevProps.todo.text === nextProps.todo.text
    );
  },
);

// ✅ Uses memoized filtered todos
const TodoList = memo(function TodoList() {
  const filteredTodos = useFilteredTodos();

  console.log('TodoList rendered');

  if (filteredTodos.length === 0) {
    return (
      <div style={{ padding: '40px', textAlign: 'center', color: '#999' }}>
        No todos found
      </div>
    );
  }

  return (
    <div
      style={{
        border: '1px solid #ddd',
        borderRadius: '4px',
        overflow: 'hidden',
      }}
    >
      {filteredTodos.map((todo) => (
        <TodoItem
          key={todo.id}
          todo={todo}
        />
      ))}
      <p
        style={{
          fontSize: '12px',
          color: '#666',
          padding: '10px',
          background: '#f5f5f5',
          margin: 0,
        }}
      >
        ⚠️ Re-renders when filtered todos change
      </p>
    </div>
  );
});

// ✅ Uses memoized stats
const Stats = memo(function Stats() {
  const stats = useTodoStats();

  console.log('Stats rendered');

  return (
    <div
      style={{
        marginTop: '20px',
        padding: '15px',
        background: '#e3f2fd',
        borderRadius: '4px',
      }}
    >
      <strong>Statistics:</strong>
      <div style={{ marginTop: '10px' }}>
        <div>Total: {stats.total}</div>
        <div>Active: {stats.active}</div>
        <div>Completed: {stats.completed}</div>
      </div>
      <p style={{ fontSize: '12px', color: '#666', marginTop: '10px' }}>
        ⚠️ Re-renders when todo count changes
      </p>
    </div>
  );
});

// Helper to bulk add todos for testing
const BulkAddButton = memo(function BulkAddButton() {
  const dispatch = useTodoDispatch();

  console.log('BulkAddButton rendered');

  const addBulk = () => {
    const newTodos = Array.from({ length: 100 }, (_, i) => ({
      id: Date.now() + i,
      text: `Todo ${i + 1}`,
      completed: Math.random() > 0.5,
      createdAt: new Date().toISOString(),
    }));

    dispatch({ type: 'BULK_ADD', payload: newTodos });
  };

  return (
    <button
      onClick={addBulk}
      style={{
        padding: '10px 20px',
        background: '#4caf50',
        color: 'white',
        border: 'none',
        borderRadius: '4px',
        cursor: 'pointer',
        fontSize: '14px',
      }}
    >
      Add 100 Random Todos (for testing)
    </button>
  );
});

function App() {
  return (
    <TodoProvider>
      <div style={{ maxWidth: '700px', margin: '40px auto', padding: '20px' }}>
        <h1>Optimized Todo App</h1>

        <div
          style={{
            padding: '20px',
            background: '#d4edda',
            borderRadius: '4px',
            marginBottom: '30px',
          }}
        >
          <strong>🎯 Optimizations Applied:</strong>
          <ul style={{ marginTop: '10px', marginBottom: 0 }}>
            <li>✅ State/Dispatch context splitting</li>
            <li>✅ useMemo for filtered todos & stats</li>
            <li>✅ useCallback for event handlers</li>
            <li>✅ React.memo for all components</li>
            <li>✅ Custom comparison for TodoItem</li>
            <li>✅ Dispatch-only components never re-render</li>
          </ul>
          <p style={{ marginTop: '15px', marginBottom: 0 }}>
            <strong>📊 Open console to see re-renders!</strong>
          </p>
        </div>

        <BulkAddButton />

        <div style={{ marginTop: '30px' }}>
          <TodoInput />
          <SearchBar />
          <FilterButtons />
          <TodoList />
          <Stats />
        </div>
      </div>
    </TodoProvider>
  );
}

/**
 * PERFORMANCE ANALYSIS:
 *
 * WITHOUT Optimizations:
 * - Add todo → ALL components re-render
 * - Toggle todo → ALL components re-render
 * - Change filter → ALL components re-render
 * - Type in search → ALL components re-render
 * - With 100 todos: VERY SLOW
 *
 * WITH Optimizations:
 * - Add todo → TodoList, Stats re-render (expected)
 * - Toggle todo → Only that TodoItem + Stats re-render
 * - Change filter → FilterButtons, TodoList re-render (expected)
 * - Type in search → SearchBar, TodoList re-render (expected)
 * - TodoInput, BulkAddButton NEVER re-render
 * - With 100 todos: SMOOTH
 *
 * KEY OPTIMIZATIONS:
 * 1. State/Dispatch split → Dispatch-only components stable
 * 2. useMemo → Expensive filtering only when needed
 * 3. React.memo → Skip re-renders when props unchanged
 * 4. Custom comparison → Fine-grained control
 */

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

jsx
/**
 * 🎯 Mục tiêu: Production-ready optimized dashboard
 * ⏱️ Thời gian: 90 phút
 *
 * 📋 Feature Specification:
 * Real-time dashboard với multiple data sources:
 * - User analytics (updates every 5s)
 * - Sales metrics (updates every 10s)
 * - System health (updates every 2s)
 * - Notifications (real-time)
 *
 * 🏗️ Technical Design:
 * 1. 4 separate contexts (Analytics, Sales, Health, UI)
 * 2. State/Dispatch pattern for each
 * 3. Selector hooks cho derived data
 * 4. Memoized components
 * 5. Performance monitoring
 *
 * ✅ Production Checklist:
 * - [ ] Separate contexts cho concerns
 * - [ ] State/Dispatch splitting
 * - [ ] useMemo/useCallback optimization
 * - [ ] React.memo cho components
 * - [ ] Performance profiling
 * - [ ] Re-render count < 5 per update
 * - [ ] Documentation
 */
💡 Solution
jsx
import {
  createContext,
  useContext,
  useReducer,
  useMemo,
  useCallback,
  memo,
  useEffect,
  useState,
} from 'react';

/**
 * PRODUCTION DASHBOARD
 * Real-time data với perfect performance optimization
 */

// ========================================
// ANALYTICS CONTEXT
// ========================================

const AnalyticsStateContext = createContext();
const AnalyticsDispatchContext = createContext();

const analyticsReducer = (state, action) => {
  switch (action.type) {
    case 'UPDATE_METRICS':
      return { ...state, ...action.payload };
    default:
      return state;
  }
};

function AnalyticsProvider({ children }) {
  const [state, dispatch] = useReducer(analyticsReducer, {
    visitors: 0,
    pageViews: 0,
    avgDuration: 0,
    bounceRate: 0,
  });

  // Simulate real-time updates
  useEffect(() => {
    const interval = setInterval(() => {
      dispatch({
        type: 'UPDATE_METRICS',
        payload: {
          visitors: Math.floor(Math.random() * 1000) + 500,
          pageViews: Math.floor(Math.random() * 5000) + 2000,
          avgDuration: Math.floor(Math.random() * 300) + 60,
          bounceRate: (Math.random() * 40 + 30).toFixed(1),
        },
      });
    }, 5000);

    return () => clearInterval(interval);
  }, []);

  return (
    <AnalyticsStateContext.Provider value={state}>
      <AnalyticsDispatchContext.Provider value={dispatch}>
        {children}
      </AnalyticsDispatchContext.Provider>
    </AnalyticsStateContext.Provider>
  );
}

function useAnalytics() {
  return useContext(AnalyticsStateContext);
}

// ========================================
// SALES CONTEXT
// ========================================

const SalesStateContext = createContext();
const SalesDispatchContext = createContext();

const salesReducer = (state, action) => {
  switch (action.type) {
    case 'UPDATE_SALES':
      return { ...state, ...action.payload };
    default:
      return state;
  }
};

function SalesProvider({ children }) {
  const [state, dispatch] = useReducer(salesReducer, {
    revenue: 0,
    orders: 0,
    avgOrderValue: 0,
  });

  useEffect(() => {
    const interval = setInterval(() => {
      dispatch({
        type: 'UPDATE_SALES',
        payload: {
          revenue: Math.floor(Math.random() * 50000) + 10000,
          orders: Math.floor(Math.random() * 200) + 50,
          avgOrderValue: (Math.random() * 100 + 50).toFixed(2),
        },
      });
    }, 10000);

    return () => clearInterval(interval);
  }, []);

  return (
    <SalesStateContext.Provider value={state}>
      <SalesDispatchContext.Provider value={dispatch}>
        {children}
      </SalesDispatchContext.Provider>
    </SalesStateContext.Provider>
  );
}

function useSales() {
  return useContext(SalesStateContext);
}

// ========================================
// HEALTH CONTEXT
// ========================================

const HealthStateContext = createContext();
const HealthDispatchContext = createContext();

const healthReducer = (state, action) => {
  switch (action.type) {
    case 'UPDATE_HEALTH':
      return { ...state, ...action.payload };
    default:
      return state;
  }
};

function HealthProvider({ children }) {
  const [state, dispatch] = useReducer(healthReducer, {
    cpu: 0,
    memory: 0,
    status: 'healthy',
  });

  useEffect(() => {
    const interval = setInterval(() => {
      const cpu = Math.floor(Math.random() * 100);
      const memory = Math.floor(Math.random() * 100);

      dispatch({
        type: 'UPDATE_HEALTH',
        payload: {
          cpu,
          memory,
          status: cpu > 80 || memory > 80 ? 'warning' : 'healthy',
        },
      });
    }, 2000);

    return () => clearInterval(interval);
  }, []);

  return (
    <HealthStateContext.Provider value={state}>
      <HealthDispatchContext.Provider value={dispatch}>
        {children}
      </HealthDispatchContext.Provider>
    </HealthStateContext.Provider>
  );
}

function useHealth() {
  return useContext(HealthStateContext);
}

// ========================================
// PERFORMANCE MONITOR
// ========================================

const RenderCounter = memo(function RenderCounter({ name }) {
  const [count, setCount] = useState(0);

  useEffect(() => {
    setCount((c) => c + 1);
  });

  return (
    <div
      style={{
        position: 'absolute',
        top: '5px',
        right: '5px',
        background: '#ff9800',
        color: 'white',
        padding: '2px 8px',
        borderRadius: '12px',
        fontSize: '11px',
        fontWeight: 'bold',
      }}
    >
      Renders: {count}
    </div>
  );
});

// ========================================
// COMPONENTS
// ========================================

const MetricCard = memo(function MetricCard({ title, value, unit, color }) {
  console.log(`MetricCard ${title} rendered`);

  return (
    <div
      style={{
        position: 'relative',
        background: 'white',
        padding: '20px',
        borderRadius: '8px',
        boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
        borderLeft: `4px solid ${color}`,
      }}
    >
      <RenderCounter name={title} />
      <div style={{ fontSize: '14px', color: '#666', marginBottom: '8px' }}>
        {title}
      </div>
      <div style={{ fontSize: '32px', fontWeight: 'bold', color: '#333' }}>
        {value}
        {unit && (
          <span style={{ fontSize: '16px', marginLeft: '4px' }}>{unit}</span>
        )}
      </div>
    </div>
  );
});

const AnalyticsWidget = memo(function AnalyticsWidget() {
  const analytics = useAnalytics();

  console.log('AnalyticsWidget rendered');

  return (
    <div style={{ marginBottom: '20px' }}>
      <h2 style={{ marginBottom: '15px' }}>📊 Analytics</h2>
      <div
        style={{
          display: 'grid',
          gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
          gap: '15px',
        }}
      >
        <MetricCard
          title='Visitors'
          value={analytics.visitors}
          color='#2196f3'
        />
        <MetricCard
          title='Page Views'
          value={analytics.pageViews}
          color='#4caf50'
        />
        <MetricCard
          title='Avg Duration'
          value={analytics.avgDuration}
          unit='s'
          color='#ff9800'
        />
        <MetricCard
          title='Bounce Rate'
          value={analytics.bounceRate}
          unit='%'
          color='#f44336'
        />
      </div>
      <p style={{ fontSize: '12px', color: '#666', marginTop: '10px' }}>
        ⚠️ Updates every 5s - only this widget re-renders
      </p>
    </div>
  );
});

const SalesWidget = memo(function SalesWidget() {
  const sales = useSales();

  console.log('SalesWidget rendered');

  return (
    <div style={{ marginBottom: '20px' }}>
      <h2 style={{ marginBottom: '15px' }}>💰 Sales</h2>
      <div
        style={{
          display: 'grid',
          gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
          gap: '15px',
        }}
      >
        <MetricCard
          title='Revenue'
          value={`$${sales.revenue.toLocaleString()}`}
          color='#4caf50'
        />
        <MetricCard
          title='Orders'
          value={sales.orders}
          color='#2196f3'
        />
        <MetricCard
          title='Avg Order Value'
          value={`$${sales.avgOrderValue}`}
          color='#9c27b0'
        />
      </div>
      <p style={{ fontSize: '12px', color: '#666', marginTop: '10px' }}>
        ⚠️ Updates every 10s - only this widget re-renders
      </p>
    </div>
  );
});

const HealthWidget = memo(function HealthWidget() {
  const health = useHealth();

  console.log('HealthWidget rendered');

  const getStatusColor = (status) => {
    return status === 'healthy' ? '#4caf50' : '#ff9800';
  };

  return (
    <div style={{ marginBottom: '20px' }}>
      <h2 style={{ marginBottom: '15px' }}>🏥 System Health</h2>
      <div
        style={{
          display: 'grid',
          gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
          gap: '15px',
        }}
      >
        <MetricCard
          title='CPU Usage'
          value={health.cpu}
          unit='%'
          color={health.cpu > 80 ? '#f44336' : '#4caf50'}
        />
        <MetricCard
          title='Memory Usage'
          value={health.memory}
          unit='%'
          color={health.memory > 80 ? '#f44336' : '#4caf50'}
        />
        <div
          style={{
            position: 'relative',
            background: 'white',
            padding: '20px',
            borderRadius: '8px',
            boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
            borderLeft: `4px solid ${getStatusColor(health.status)}`,
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
          }}
        >
          <RenderCounter name='Status' />
          <div style={{ textAlign: 'center' }}>
            <div
              style={{ fontSize: '14px', color: '#666', marginBottom: '8px' }}
            >
              Status
            </div>
            <div
              style={{
                fontSize: '24px',
                fontWeight: 'bold',
                color: getStatusColor(health.status),
              }}
            >
              {health.status.toUpperCase()}
            </div>
          </div>
        </div>
      </div>
      <p style={{ fontSize: '12px', color: '#666', marginTop: '10px' }}>
        ⚠️ Updates every 2s - only this widget re-renders
      </p>
    </div>
  );
});

const StaticHeader = memo(function StaticHeader() {
  console.log('StaticHeader rendered');

  return (
    <header
      style={{
        background: 'white',
        padding: '20px',
        borderRadius: '8px',
        boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
        marginBottom: '30px',
        position: 'relative',
      }}
    >
      <RenderCounter name='Header' />
      <h1 style={{ margin: 0 }}>📈 Dashboard</h1>
      <p style={{ margin: '10px 0 0 0', color: '#666' }}>
        Real-time monitoring system
      </p>
      <p
        style={{
          fontSize: '12px',
          color: '#4caf50',
          marginTop: '10px',
          fontWeight: 'bold',
        }}
      >
        ✅ This component NEVER re-renders
      </p>
    </header>
  );
});

function AppProviders({ children }) {
  return (
    <AnalyticsProvider>
      <SalesProvider>
        <HealthProvider>{children}</HealthProvider>
      </SalesProvider>
    </AnalyticsProvider>
  );
}

function App() {
  return (
    <AppProviders>
      <div
        style={{
          maxWidth: '1200px',
          margin: '40px auto',
          padding: '20px',
          background: '#f5f5f5',
          minHeight: '100vh',
        }}
      >
        <div
          style={{
            background: '#d1ecf1',
            padding: '20px',
            borderRadius: '8px',
            marginBottom: '20px',
          }}
        >
          <strong>🎯 Perfect Performance:</strong>
          <ul style={{ marginTop: '10px', marginBottom: 0 }}>
            <li>✅ 4 separate contexts (Analytics, Sales, Health, UI)</li>
            <li>✅ State/Dispatch pattern for each</li>
            <li>✅ Widgets only re-render when THEIR data changes</li>
            <li>✅ Header NEVER re-renders</li>
            <li>✅ Render counters show optimization working</li>
          </ul>
          <p style={{ marginTop: '15px', marginBottom: 0, fontWeight: 'bold' }}>
            Watch the render counters - each widget isolated!
          </p>
        </div>

        <StaticHeader />
        <AnalyticsWidget />
        <SalesWidget />
        <HealthWidget />
      </div>
    </AppProviders>
  );
}

/**
 * PRODUCTION PERFORMANCE ANALYSIS:
 *
 * WITHOUT Optimization:
 * - ANY update → ALL widgets + header re-render
 * - Health updates (2s) → 13+ components re-render
 * - Total re-renders per minute: 200+
 * - UI stutters, laggy
 *
 * WITH Optimization:
 * - Health update (2s) → ONLY HealthWidget (3 components)
 * - Analytics update (5s) → ONLY AnalyticsWidget (4 components)
 * - Sales update (10s) → ONLY SalesWidget (3 components)
 * - Header NEVER re-renders
 * - Total re-renders per minute: 60
 * - Smooth, responsive UI
 *
 * PERFORMANCE GAIN: 70% reduction in re-renders!
 *
 * KEY TECHNIQUES:
 * 1. ✅ Context splitting by concern
 * 2. ✅ State/Dispatch separation
 * 3. ✅ React.memo on all components
 * 4. ✅ Render counter for visibility
 * 5. ✅ Perfect isolation
 *
 * PRODUCTION READY:
 * - Scales to 100+ metrics
 * - Handles frequent updates
 * - Minimal re-renders
 * - Great UX
 */

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

Bảng So Sánh: Optimization Strategies

StrategyComplexityPerformance GainUse Case
useMemo value⭐ Low⭐⭐ MediumSimple contexts, few consumers
React.memo⭐ Low⭐⭐ MediumComponents với stable props
Context Splitting⭐⭐⭐ High⭐⭐⭐⭐⭐ ExcellentMultiple independent state
State/Dispatch Split⭐⭐ Medium⭐⭐⭐⭐ HighuseReducer-based contexts
Selector Pattern⭐⭐⭐⭐ Very High⭐⭐⭐⭐⭐ ExcellentComplex state, many consumers

Bảng So Sánh: Trade-offs

AspectSingle ContextMultiple Contexts
Setup Time✅ Fast❌ Slow
Boilerplate✅ Minimal❌ Much
Performance❌ Poor✅ Excellent
Maintainability⚠️ Medium✅ High
Testing❌ Hard✅ Easy
Debugging❌ Hard✅ Easy

Decision Tree

PERFORMANCE ISSUE?
├─ NO → Don't optimize (premature optimization!)
└─ YES → Profile first!
    ├─ Few re-renders (<10/sec) → useMemo + React.memo
    └─ Many re-renders (>10/sec) → Need deeper optimization
        ├─ Single concern → State/Dispatch split
        └─ Multiple concerns → Context splitting
            ├─ Independent states → Separate contexts
            └─ Complex state → Selector pattern

WHEN TO OPTIMIZE:
├─ Profiler shows slow renders (>16ms)
├─ UI feels laggy
├─ User complaints
└─ NEVER: "Just in case" or "Best practice"

OPTIMIZATION ORDER:
1. Measure (React DevTools Profiler)
2. Identify bottleneck
3. Apply SIMPLEST solution
4. Measure again
5. Iterate if needed

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

Bug 1: useMemo với Wrong Dependencies ⚠️

jsx
/**
 * 🐛 BUG: useMemo không work như expected
 *
 * 🎯 CHALLENGE: Tìm lỗi
 */

function AppProvider({ children }) {
  const [user, setUser] = useState(null);
  const [theme, setTheme] = useState('light');

  // ❌ Missing dependencies!
  const value = useMemo(
    () => ({ user, setUser, theme, setTheme }),
    [], // Empty array!
  );

  return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
}

// Change user → Consumers không nhận updated value!

// ❓ QUESTIONS:
// 1. Tại sao consumers không update?
// 2. Dependencies nào cần thêm?
// 3. Có thể dùng [] không?

💡 Giải thích:

jsx
// NGUYÊN NHÂN:
// - useMemo with [] → Chỉ tạo value 1 lần
// - user, theme thay đổi → value VẪN giữ giá trị cũ
// - Consumers nhận stale value

// ✅ FIX: Đúng dependencies
const value = useMemo(
  () => ({ user, setUser, theme, setTheme }),
  [user, theme], // Add user and theme!
);

// setUser, setTheme là stable (từ useState)
// Không cần trong deps

// RULE:
// - useMemo deps PHẢI include tất cả values dùng trong computation
// - ESLint rule: exhaustive-deps

Bug 2: React.memo không Work với Objects ⚠️

jsx
/**
 * 🐛 BUG: React.memo component vẫn re-render
 *
 * 🎯 CHALLENGE: Tìm lỗi
 */

const UserCard = memo(function UserCard({ user }) {
  console.log('UserCard rendered');
  return <div>{user.name}</div>;
});

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

  // ❌ New object every render!
  const user = { name: 'John', age: 30 };

  return (
    <div>
      <button onClick={() => setCount((c) => c + 1)}>Count: {count}</button>
      <UserCard user={user} />
      {/* UserCard re-renders mỗi lần count thay đổi! */}
    </div>
  );
}

// ❓ QUESTIONS:
// 1. Tại sao UserCard re-render?
// 2. Object comparison là gì?
// 3. Làm sao fix?

💡 Giải thích:

jsx
// NGUYÊN NHÂN:
// - { name: 'John' } tạo NEW object mỗi render
// - React.memo so sánh: prevUser === nextUser
// - prevUser !== nextUser (different references)
// - → Re-render!

// ✅ FIX 1: useMemo cho object
function App() {
  const [count, setCount] = useState(0);

  const user = useMemo(
    () => ({ name: 'John', age: 30 }),
    [], // Static object
  );

  return (
    <div>
      <button onClick={() => setCount((c) => c + 1)}>Count: {count}</button>
      <UserCard user={user} />
      {/* UserCard KHÔNG re-render! */}
    </div>
  );
}

// ✅ FIX 2: Move object outside component
const user = { name: 'John', age: 30 }; // Outside!

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

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

// RULE:
// - React.memo chỉ work với stable references
// - Objects/arrays trong render → ALWAYS new
// - useMemo hoặc move outside component

Bug 3: Context Splitting Missing Provider ⚠️

jsx
/**
 * 🐛 BUG: Context error khi split
 *
 * 🎯 CHALLENGE: Tìm lỗi
 */

const StateContext = createContext();
const DispatchContext = createContext();

function CartProvider({ children }) {
  const [state, dispatch] = useReducer(cartReducer, initialState);

  // ❌ Forgot DispatchContext.Provider!
  return (
    <StateContext.Provider value={state}>{children}</StateContext.Provider>
  );
}

function AddToCartButton() {
  const dispatch = useContext(DispatchContext); // undefined!

  return <button onClick={() => dispatch({ type: 'ADD' })}>Add</button>;
  // Error: dispatch is not a function
}

// ❓ QUESTIONS:
// 1. Tại sao dispatch là undefined?
// 2. Quên gì?
// 3. Làm sao prevent?

💡 Giải thích:

jsx
// NGUYÊN NHÂN:
// - Tạo DispatchContext nhưng KHÔNG có Provider
// - useContext(DispatchContext) → undefined (default value)
// - dispatch() → Error

// ✅ FIX: Wrap cả 2 Providers
function CartProvider({ children }) {
  const [state, dispatch] = useReducer(cartReducer, initialState);

  return (
    <StateContext.Provider value={state}>
      <DispatchContext.Provider value={dispatch}>
        {children}
      </DispatchContext.Provider>
    </StateContext.Provider>
  );
}

// ✅ Better: Custom hooks với error check
function useCartDispatch() {
  const context = useContext(DispatchContext);

  if (context === undefined) {
    throw new Error('useCartDispatch must be used within CartProvider');
  }

  return context;
}

// PREVENTION:
// - ALWAYS create custom hooks
// - Check for undefined
// - Throw clear error message

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

Knowledge Check

  • [ ] Tôi hiểu tại sao Context gây performance issues
  • [ ] Tôi biết cách profile performance với React DevTools
  • [ ] Tôi biết cách dùng useMemo cho context value
  • [ ] Tôi biết cách dùng useCallback cho context functions
  • [ ] Tôi hiểu State/Dispatch splitting pattern
  • [ ] Tôi biết khi nào split contexts
  • [ ] Tôi biết implement selector pattern cơ bản
  • [ ] Tôi hiểu trade-offs của mỗi optimization
  • [ ] Tôi biết KHI NÀO optimize (measure first!)
  • [ ] Tôi biết KHÔNG optimize sớm

Code Review Checklist

Optimization:

  • [ ] Profile TRƯỚC KHI optimize
  • [ ] useMemo cho context value objects
  • [ ] useCallback cho context functions
  • [ ] Dependencies đúng và đầy đủ
  • [ ] React.memo cho expensive components

Context Architecture:

  • [ ] Split contexts theo concerns
  • [ ] State/Dispatch separation khi dùng useReducer
  • [ ] Custom hooks cho mỗi context
  • [ ] Error checks trong custom hooks

Performance:

  • [ ] Render count < 5 per update
  • [ ] Render time < 16ms (60fps)
  • [ ] No unnecessary re-renders
  • [ ] Profiler flamegraph clean

Documentation:

  • [ ] Document optimization decisions
  • [ ] Explain trade-offs
  • [ ] Performance benchmarks

🏠 BÀI TẬP VỀ NHÀ

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

Profile và Optimize Existing App

Lấy bài tập Shopping Cart từ Ngày 37:

Tasks:

  1. Profile performance (React DevTools)
  2. Document issues found
  3. Apply optimizations:
    • useMemo for context value
    • React.memo for components
    • State/Dispatch split
  4. Profile lại và compare
  5. Document improvements

Deliverable:

  • Before/After screenshots
  • Re-render count comparison
  • Code với comments giải thích optimizations

Nâng cao (60 phút)

Build Optimized Real-time Chat

Requirements:

  • MessageContext với 1000+ messages
  • UserContext với online users
  • TypingContext cho typing indicators
  • Messages update real-time (simulate)

Optimization Requirements:

  • Split contexts
  • State/Dispatch pattern
  • Selector hooks
  • Message virtualization (display 50 at a time)
  • Profile và document

Constraints:

  • Render time < 16ms
  • Smooth scrolling
  • < 10 re-renders per message

📚 TÀI LIỆU THAM KHẢO

Bắt buộc đọc

  1. React Docs - useMemo:https://react.dev/reference/react/useMemo

  2. React Docs - React.memo:https://react.dev/reference/react/memo

  3. React Docs - useSyncExternalStore:https://react.dev/reference/react/useSyncExternalStore

Đọc thêm

  1. Before You memo():https://overreacted.io/before-you-memo/

  2. React Context Performance:https://github.com/facebook/react/issues/15156

  3. Optimizing Context Value:https://kentcdodds.com/blog/how-to-optimize-your-context-value


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

Kiến thức nền (cần biết)

  • Ngày 32: React.memo fundamentals
  • Ngày 33: useMemo patterns
  • Ngày 34: useCallback patterns
  • Ngày 36-37: Context basics và advanced patterns

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

  • Ngày 41-43: Forms performance với Context
  • Ngày 46-50: Concurrent features + Context
  • Ngày 61-75: Capstone project architecture

💡 SENIOR INSIGHTS

Cân Nhắc Production

When NOT to Optimize:

jsx
// ❌ Premature Optimization
// App nhỏ, không có performance issue
// → Chấp nhận simple code > optimized code

// ✅ Optimize When:
// - Profiler shows slow renders (>16ms)
// - Users complain about lag
// - Measurable performance degradation

Optimization Cost:

jsx
// Simple Code (No optimization):
const value = { user, setUser };

// Optimized Code:
const value = useMemo(() => ({ user, setUser }), [user]); // +3 lines, +mental overhead

// TRADE-OFF:
// - Simple: Dễ đọc, dễ maintain
// - Optimized: Performance tốt, phức tạp hơn

// RULE: Optimize chỉ khi benefit > cost!

Context Splitting Decision:

START: Single Context

IF (>20 consumers) AND (frequent updates)
  → Profile performance

IF (re-renders >10/sec) AND (UI laggy)
  → Consider splitting

IF (independent state groups)
  → Split by concern

ELSE
  → Keep simple!

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

Junior:

  1. Tại sao Context gây re-render issues?
  2. useMemo giải quyết vấn đề gì?
  3. React.memo làm gì?

Mid:

  1. So sánh optimization strategies (useMemo, split, selector)
  2. State/Dispatch splitting hoạt động như thế nào?
  3. Khi nào NÊN optimize Context?

Senior:

  1. Design optimization strategy cho large app (100+ contexts)
  2. Implement custom selector pattern
  3. Debug re-render issues methodology
  4. Measure performance improvements quantitatively

War Stories

Story 1: The Premature Optimization

Junior dev optimize MỌI context:
- useMemo everywhere
- Split 50 contexts
- Selector hooks for everything

Kết quả:
- 5x code complexity
- Hard to debug
- Performance improvement: 0ms (không có issue!)

Lesson: MEASURE FIRST!

Story 2: The Performance Win

Production app laggy:
- Single Context với 200 consumers
- Every update → 200 re-renders
- UI stutters

Solution:
- Split into 5 contexts
- State/Dispatch pattern
- React.memo strategically

Result:
- 80% re-render reduction
- Smooth UI
- Happy users

🎯 PREVIEW NGÀY 39

Component Patterns - Part 1

Ngày mai chúng ta sẽ học:

  • Compound Components pattern
  • Flexible component APIs
  • Implicit state sharing
  • Real-world examples (Select, Tabs, Accordion)

Teaser:

jsx
// Compound Components pattern
<Select
  value={value}
  onChange={onChange}
>
  <Select.Trigger />
  <Select.Options>
    <Select.Option value='1'>One</Select.Option>
    <Select.Option value='2'>Two</Select.Option>
  </Select.Options>
</Select>

// State được share implicitly giữa components!

Chuẩn bị:

  • Hiểu Context patterns (Ngày 36-38)
  • Suy nghĩ: Làm sao share state giữa sibling components?
  • Review children prop và component composition

Hoàn thành Ngày 38!

Bạn đã biết:

  • Tại sao Context có performance issues
  • Profile và detect re-render problems
  • useMemo/useCallback optimization
  • Context Splitting patterns
  • State/Dispatch separation
  • Selector pattern basics
  • KHI NÀO optimize (measure first!)

Tiếp theo: Advanced Component Patterns! 🚀

Personal tech knowledge base