Skip to content

📅 NGÀY 26: useReducer - Quản Lý State Phức Tạp với Reducer Pattern

📍 Vị trí: Phase 3, Tuần 6, Ngày 26/45

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


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

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

  • [ ] Hiểu được reducer pattern là gì và tại sao nó quan trọng
  • [ ] Viết được reducer function thuần túy (pure function)
  • [ ] Sử dụng được useReducer hook để quản lý complex state
  • [ ] Quyết định được khi nào dùng useState vs useReducer dựa trên trade-offs
  • [ ] Thiết kế được actions và action creators chuẩn mực

🤔 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:

  1. useState có vấn đề gì khi state logic phức tạp?

    • Gợi ý: Nghĩ về form có 10+ fields, mỗi field có validation...
  2. Pure function là gì? Cho ví dụ.

    • Gợi ý: Function không side effects, same input → same output
  3. Bạn đã từng gặp trường hợp state updates phụ thuộc vào nhau chưa?

    • Ví dụ: Submit form → loading true → data fetched → loading false, data updated

📖 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 build form đăng ký user với requirements sau:

jsx
// ❌ VẤN ĐỀ: useState cho complex state
function RegistrationForm() {
  const [username, setUsername] = useState('');
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [confirmPassword, setConfirmPassword] = useState('');
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);
  const [touched, setTouched] = useState({});
  const [validationErrors, setValidationErrors] = useState({});

  const handleSubmit = async (e) => {
    e.preventDefault();

    // 😱 Logic phức tạp với nhiều state updates
    setIsLoading(true);
    setError(null);

    try {
      // Validation
      const errors = {};
      if (!username) errors.username = 'Required';
      if (!email) errors.email = 'Required';
      if (password !== confirmPassword) {
        errors.password = 'Passwords must match';
      }

      if (Object.keys(errors).length > 0) {
        setValidationErrors(errors);
        setIsLoading(false); // ⚠️ Dễ quên!
        return;
      }

      // API call
      await registerUser({ username, email, password });

      // Success - reset form
      setUsername('');
      setEmail('');
      setPassword('');
      setConfirmPassword('');
      setTouched({});
      setValidationErrors({});
      setIsLoading(false);
    } catch (err) {
      setError(err.message);
      setIsLoading(false); // ⚠️ Duplicate logic!
    }
  };

  // ... 😰 Còn handleBlur, handleChange, etc.
}

Vấn đề:

  1. 🔴 Quá nhiều useState → khó tracking
  2. 🔴 State updates rải rác → dễ miss, duplicate logic
  3. 🔴 Khó test → phải mock từng setter
  4. 🔴 Khó debug → không biết state thay đổi bởi action nào

1.2 Giải Pháp: Reducer Pattern

Reducer Pattern là pattern tập trung tất cả state logic vào 1 chỗ:

Current State + Action → Reducer → Next State

Core Idea:

jsx
// Thay vì:
setUsername('john');
setEmail('john@example.com');
setIsLoading(true);

// Ta có:
dispatch({ type: 'UPDATE_FIELD', field: 'username', value: 'john' });
dispatch({ type: 'UPDATE_FIELD', field: 'email', value: 'john@example.com' });
dispatch({ type: 'SUBMIT_START' });

Lợi ích:

  • Centralized logic → tất cả state transitions ở 1 chỗ
  • Predictable → same action + same state = same result
  • Testable → test reducer như function thuần
  • Debuggable → log actions để biết "what happened"

1.3 Mental Model

┌─────────────────────────────────────────────────┐
│                  COMPONENT                       │
│                                                  │
│  User Event                                      │
│      ↓                                           │
│  dispatch(action) ───────────────┐              │
│      ↓                            │              │
│  ┌──────────────────┐             │              │
│  │  Current State   │             │              │
│  │  { count: 5 }    │             │              │
│  └──────────────────┘             │              │
│           │                       │              │
│           ↓                       ↓              │
│  ┌─────────────────────────────────────┐        │
│  │         REDUCER (Pure Function)      │        │
│  │                                      │        │
│  │  (state, action) => {                │        │
│  │    switch(action.type) {             │        │
│  │      case 'INCREMENT':               │        │
│  │        return { count: state.count+1 }│        │
│  │    }                                 │        │
│  │  }                                   │        │
│  └─────────────────────────────────────┘        │
│           │                                      │
│           ↓                                      │
│  ┌──────────────────┐                           │
│  │   Next State     │                           │
│  │  { count: 6 }    │                           │
│  └──────────────────┘                           │
│           │                                      │
│           ↓                                      │
│      Re-render                                   │
│                                                  │
└─────────────────────────────────────────────────┘

Analogy: Reducer giống như máy bán hàng tự động

  • State: Số tiền trong máy
  • Action: Người bấm nút "Coke"
  • Reducer: Logic "nếu bấm Coke VÀ đủ tiền → trả Coke, giảm tiền"
  • Next State: Tiền sau khi mua

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

Hiểu lầm 1: "Reducer chỉ dùng khi state phức tạp"

  • Sự thật: Reducer giúp code dễ đọc hơn, kể cả state đơn giản

Hiểu lầm 2: "Reducer phải return object mới hoàn toàn"

  • Sự thật: Reducer chỉ cần return immutable update, có thể spread

Hiểu lầm 3: "useReducer thay thế useState hoàn toàn"

  • Sự thật: Mỗi cái có use case riêng (sẽ so sánh sau)

Hiểu lầm 4: "Reducer có thể mutate state"

  • Sự thật: Reducer PHẢI pure, không side effects

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

Demo 1: Pattern Cơ Bản - Counter với useReducer ⭐

jsx
import { useReducer } from 'react';

// 1️⃣ ĐỊNH NGHĨA REDUCER
// Reducer là pure function: (currentState, action) => nextState
function counterReducer(state, action) {
  // state: current state value
  // action: object mô tả "what happened"

  switch (action.type) {
    case 'INCREMENT':
      return { count: state.count + 1 };

    case 'DECREMENT':
      return { count: state.count - 1 };

    case 'RESET':
      return { count: 0 };

    case 'SET':
      return { count: action.payload };

    default:
      // ⚠️ QUAN TRỌNG: Luôn có default case
      // Throw error nếu action không hợp lệ
      throw new Error(`Unknown action type: ${action.type}`);
  }
}

// 2️⃣ SỬ DỤNG useReducer TRONG COMPONENT
function Counter() {
  // useReducer(reducer, initialState)
  // Returns: [state, dispatch]
  const [state, dispatch] = useReducer(
    counterReducer,
    { count: 0 }, // initial state
  );

  return (
    <div>
      <h1>Count: {state.count}</h1>

      {/* 3️⃣ DISPATCH ACTIONS */}
      <button onClick={() => dispatch({ type: 'INCREMENT' })}>+1</button>

      <button onClick={() => dispatch({ type: 'DECREMENT' })}>-1</button>

      <button onClick={() => dispatch({ type: 'RESET' })}>Reset</button>

      <button onClick={() => dispatch({ type: 'SET', payload: 10 })}>
        Set to 10
      </button>
    </div>
  );
}

📝 Giải thích:

  1. Reducer Function:

    • Pure function, không side effects
    • Input: (state, action)
    • Output: new state (immutable)
  2. useReducer Hook:

    • const [state, dispatch] = useReducer(reducer, initialState)
    • state: current state (giống useState)
    • dispatch: function để gửi actions
  3. Action Object:

    • { type: 'ACTION_NAME' } - required
    • { type: 'ACTION_NAME', payload: data } - với data

🔍 So sánh useState vs useReducer:

jsx
// ❌ VỚI useState
function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>+1</button>
      <button onClick={() => setCount(count - 1)}>-1</button>
      <button onClick={() => setCount(0)}>Reset</button>
      <button onClick={() => setCount(10)}>Set to 10</button>
    </div>
  );
}

// ✅ VỚI useReducer
// Logic tập trung, dễ đọc, dễ test
function Counter() {
  const [state, dispatch] = useReducer(counterReducer, { count: 0 });

  return (
    <div>
      <button onClick={() => dispatch({ type: 'INCREMENT' })}>+1</button>
      <button onClick={() => dispatch({ type: 'DECREMENT' })}>-1</button>
      <button onClick={() => dispatch({ type: 'RESET' })}>Reset</button>
      <button onClick={() => dispatch({ type: 'SET', payload: 10 })}>
        Set to 10
      </button>
    </div>
  );
}

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

jsx
// 📋 TODO REDUCER
function todoReducer(state, action) {
  switch (action.type) {
    case 'ADD_TODO':
      return {
        ...state,
        todos: [
          ...state.todos,
          {
            id: Date.now(),
            text: action.payload.text,
            completed: false,
          },
        ],
      };

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

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

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

    case 'CLEAR_COMPLETED':
      return {
        ...state,
        todos: state.todos.filter((todo) => !todo.completed),
      };

    default:
      throw new Error(`Unknown action: ${action.type}`);
  }
}

// 🎯 COMPONENT
function TodoApp() {
  const initialState = {
    todos: [],
    filter: 'all', // 'all' | 'active' | 'completed'
  };

  const [state, dispatch] = useReducer(todoReducer, initialState);
  const [inputValue, setInputValue] = useState('');

  // ✅ Event handlers gọi dispatch
  const handleAddTodo = (e) => {
    e.preventDefault();
    if (inputValue.trim()) {
      dispatch({
        type: 'ADD_TODO',
        payload: { text: inputValue },
      });
      setInputValue('');
    }
  };

  const handleToggle = (id) => {
    dispatch({
      type: 'TOGGLE_TODO',
      payload: { id },
    });
  };

  const handleDelete = (id) => {
    dispatch({
      type: 'DELETE_TODO',
      payload: { id },
    });
  };

  // ✅ Derived state (computed from state)
  const filteredTodos = state.todos.filter((todo) => {
    if (state.filter === 'active') return !todo.completed;
    if (state.filter === 'completed') return todo.completed;
    return true; // 'all'
  });

  const activeCount = state.todos.filter((t) => !t.completed).length;
  const completedCount = state.todos.filter((t) => t.completed).length;

  return (
    <div>
      <h1>Todo App</h1>

      {/* Form */}
      <form onSubmit={handleAddTodo}>
        <input
          value={inputValue}
          onChange={(e) => setInputValue(e.target.value)}
          placeholder='What needs to be done?'
        />
        <button type='submit'>Add</button>
      </form>

      {/* Filters */}
      <div>
        <button
          onClick={() =>
            dispatch({ type: 'SET_FILTER', payload: { filter: 'all' } })
          }
          disabled={state.filter === 'all'}
        >
          All ({state.todos.length})
        </button>
        <button
          onClick={() =>
            dispatch({ type: 'SET_FILTER', payload: { filter: 'active' } })
          }
          disabled={state.filter === 'active'}
        >
          Active ({activeCount})
        </button>
        <button
          onClick={() =>
            dispatch({ type: 'SET_FILTER', payload: { filter: 'completed' } })
          }
          disabled={state.filter === 'completed'}
        >
          Completed ({completedCount})
        </button>
      </div>

      {/* Todo List */}
      <ul>
        {filteredTodos.map((todo) => (
          <li key={todo.id}>
            <input
              type='checkbox'
              checked={todo.completed}
              onChange={() => handleToggle(todo.id)}
            />
            <span
              style={{
                textDecoration: todo.completed ? 'line-through' : 'none',
              }}
            >
              {todo.text}
            </span>
            <button onClick={() => handleDelete(todo.id)}>Delete</button>
          </li>
        ))}
      </ul>

      {/* Actions */}
      {completedCount > 0 && (
        <button onClick={() => dispatch({ type: 'CLEAR_COMPLETED' })}>
          Clear Completed
        </button>
      )}
    </div>
  );
}

🎯 Tại sao useReducer tốt hơn ở đây?

  1. State shape phức tạp: { todos: [], filter: 'all' }
  2. Multiple related updates: Toggle todo → update todos array
  3. Predictable state transitions: Mỗi action có logic rõ ràng
  4. Easy to test: Test reducer như pure function

Demo 3: Edge Cases - Validation & Error Handling ⭐⭐⭐

jsx
// 🔐 FORM REDUCER với Validation
function formReducer(state, action) {
  switch (action.type) {
    case 'UPDATE_FIELD':
      // ✅ Validate on field update
      const errors = { ...state.errors };

      // Clear error khi user sửa
      if (errors[action.payload.field]) {
        delete errors[action.payload.field];
      }

      return {
        ...state,
        values: {
          ...state.values,
          [action.payload.field]: action.payload.value,
        },
        errors,
        touched: {
          ...state.touched,
          [action.payload.field]: true,
        },
      };

    case 'SUBMIT_START':
      // ⚠️ EDGE CASE: Không submit nếu đang loading
      if (state.isLoading) {
        console.warn('Already submitting');
        return state;
      }

      return {
        ...state,
        isLoading: true,
        submitError: null,
      };

    case 'SUBMIT_SUCCESS':
      // ✅ Reset form after success
      return {
        values: { username: '', email: '', password: '' },
        errors: {},
        touched: {},
        isLoading: false,
        submitError: null,
      };

    case 'SUBMIT_ERROR':
      return {
        ...state,
        isLoading: false,
        submitError: action.payload.error,
      };

    case 'SET_ERRORS':
      // ⚠️ EDGE CASE: Validation errors
      return {
        ...state,
        errors: action.payload.errors,
        isLoading: false,
      };

    case 'RESET_FORM':
      // ✅ Reset về initial state
      return action.payload.initialState;

    default:
      throw new Error(`Unknown action: ${action.type}`);
  }
}

// 🎯 COMPONENT với Error Handling
function RegistrationForm() {
  const initialState = {
    values: {
      username: '',
      email: '',
      password: '',
    },
    errors: {},
    touched: {},
    isLoading: false,
    submitError: null,
  };

  const [state, dispatch] = useReducer(formReducer, initialState);

  // ✅ Validation logic (pure function)
  const validate = (values) => {
    const errors = {};

    if (!values.username) {
      errors.username = 'Username is required';
    } else if (values.username.length < 3) {
      errors.username = 'Username must be at least 3 characters';
    }

    if (!values.email) {
      errors.email = 'Email is required';
    } else if (!/\S+@\S+\.\S+/.test(values.email)) {
      errors.email = 'Email is invalid';
    }

    if (!values.password) {
      errors.password = 'Password is required';
    } else if (values.password.length < 6) {
      errors.password = 'Password must be at least 6 characters';
    }

    return errors;
  };

  const handleSubmit = async (e) => {
    e.preventDefault();

    // Validate
    const validationErrors = validate(state.values);

    if (Object.keys(validationErrors).length > 0) {
      dispatch({
        type: 'SET_ERRORS',
        payload: { errors: validationErrors },
      });
      return;
    }

    // Submit
    dispatch({ type: 'SUBMIT_START' });

    try {
      await registerUser(state.values);
      dispatch({ type: 'SUBMIT_SUCCESS' });
      alert('Registration successful!');
    } catch (error) {
      dispatch({
        type: 'SUBMIT_ERROR',
        payload: { error: error.message },
      });
    }
  };

  const handleChange = (field) => (e) => {
    dispatch({
      type: 'UPDATE_FIELD',
      payload: {
        field,
        value: e.target.value,
      },
    });
  };

  return (
    <form onSubmit={handleSubmit}>
      {/* Username */}
      <div>
        <input
          type='text'
          value={state.values.username}
          onChange={handleChange('username')}
          placeholder='Username'
        />
        {state.touched.username && state.errors.username && (
          <span style={{ color: 'red' }}>{state.errors.username}</span>
        )}
      </div>

      {/* Email */}
      <div>
        <input
          type='email'
          value={state.values.email}
          onChange={handleChange('email')}
          placeholder='Email'
        />
        {state.touched.email && state.errors.email && (
          <span style={{ color: 'red' }}>{state.errors.email}</span>
        )}
      </div>

      {/* Password */}
      <div>
        <input
          type='password'
          value={state.values.password}
          onChange={handleChange('password')}
          placeholder='Password'
        />
        {state.touched.password && state.errors.password && (
          <span style={{ color: 'red' }}>{state.errors.password}</span>
        )}
      </div>

      {/* Submit Error */}
      {state.submitError && (
        <div style={{ color: 'red', fontWeight: 'bold' }}>
          {state.submitError}
        </div>
      )}

      {/* Buttons */}
      <button
        type='submit'
        disabled={state.isLoading}
      >
        {state.isLoading ? 'Submitting...' : 'Register'}
      </button>

      <button
        type='button'
        onClick={() =>
          dispatch({
            type: 'RESET_FORM',
            payload: { initialState },
          })
        }
      >
        Reset
      </button>
    </form>
  );
}

// Mock API
const registerUser = (data) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (data.username === 'error') {
        reject(new Error('Username already exists'));
      } else {
        resolve({ success: true });
      }
    }, 1000);
  });
};

🎯 Key Takeaways:

  1. Edge cases handled:

    • ✅ Prevent double submit (isLoading check)
    • ✅ Clear errors on field change
    • ✅ Validation before submit
    • ✅ Reset form after success
  2. State transitions rõ ràng:

    • SUBMIT_START → isLoading = true
    • SUBMIT_SUCCESS → reset form
    • SUBMIT_ERROR → show error, isLoading = false
  3. Pure functions:

    • validate() là pure function
    • Reducer không có side effects

🔨 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: Chuyển đổi useState sang useReducer
 * ⏱️ Thời gian: 15 phút
 * 🚫 KHÔNG dùng: Context, useEffect (chưa cần)
 *
 * Requirements:
 * 1. Chuyển component dưới từ useState → useReducer
 * 2. Implement 3 actions: LIKE, UNLIKE, RESET
 * 3. Reducer phải có default case throw error
 *
 * 💡 Gợi ý:
 * - Tạo reducer function trước
 * - Định nghĩa action types
 * - Thay useState bằng useReducer
 */

// ❌ CÁCH SAI: Không validate action type
function likeReducer(state, action) {
  // 🚫 Missing default case
  if (action.type === 'LIKE') {
    return { likes: state.likes + 1 };
  }
  if (action.type === 'UNLIKE') {
    return { likes: Math.max(0, state.likes - 1) };
  }
  // ⚠️ Typo "RESETT" sẽ không throw error!
  return state; // Silent fail
}

// ✅ CÁCH ĐÚNG: Luôn có default case
function likeReducer(state, action) {
  switch (action.type) {
    case 'LIKE':
      return { likes: state.likes + 1 };
    case 'UNLIKE':
      return { likes: Math.max(0, state.likes - 1) };
    case 'RESET':
      return { likes: 0 };
    default:
      throw new Error(`Unknown action type: ${action.type}`);
  }
}

// 🎯 NHIỆM VỤ CỦA BẠN: Chuyển component này sang useReducer
function LikeButton() {
  const [likes, setLikes] = useState(0);

  return (
    <div>
      <h2>❤️ {likes} likes</h2>
      <button onClick={() => setLikes(likes + 1)}>Like</button>
      <button onClick={() => setLikes(Math.max(0, likes - 1))}>Unlike</button>
      <button onClick={() => setLikes(0)}>Reset</button>
    </div>
  );
}

// TODO: Viết lại component trên với useReducer
// function LikeButton() {
//   // Your code here
// }
💡 Solution
jsx
/**
 * LikeButton component using useReducer instead of multiple useState calls
 * Manages like count with actions: LIKE, UNLIKE, RESET
 */
import { useReducer } from 'react';

// 1. Reducer function - pure, predictable state transitions
function likeReducer(state, action) {
  switch (action.type) {
    case 'LIKE':
      return { likes: state.likes + 1 };

    case 'UNLIKE':
      return { likes: Math.max(0, state.likes - 1) };

    case 'RESET':
      return { likes: 0 };

    default:
      throw new Error(`Unknown action type: ${action.type}`);
  }
}

function LikeButton() {
  const [state, dispatch] = useReducer(likeReducer, { likes: 0 });

  return (
    <div>
      <h2>❤️ {state.likes} likes</h2>
      <button onClick={() => dispatch({ type: 'LIKE' })}>Like</button>
      <button onClick={() => dispatch({ type: 'UNLIKE' })}>Unlike</button>
      <button onClick={() => dispatch({ type: 'RESET' })}>Reset</button>
    </div>
  );
}

export default LikeButton;

/*
Ví dụ kết quả khi tương tác:
Ban đầu:          ❤️ 0 likes
Nhấn Like ×3:     ❤️ 3 likes
Nhấn Unlike ×2:   ❤️ 1 like
Nhấn Unlike ×2:   ❤️ 0 likes (không âm)
Nhấn Reset:       ❤️ 0 likes
Nhấn Unlike khi 0: vẫn ❤️ 0 likes (Math.max bảo vệ)
*/

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

jsx
/**
 * 🎯 Mục tiêu: Quyết định useState vs useReducer
 * ⏱️ Thời gian: 25 phút
 *
 * Scenario: Bạn cần build Shopping Cart
 *
 * State cần quản lý:
 * - items: [{ id, name, price, quantity }]
 * - totalPrice: number
 * - discountCode: string
 * - discountAmount: number
 *
 * Actions:
 * - ADD_ITEM
 * - REMOVE_ITEM
 * - UPDATE_QUANTITY
 * - APPLY_DISCOUNT
 * - CLEAR_CART
 *
 * 🤔 PHÂN TÍCH:
 *
 * Approach A: Multiple useState
 * const [items, setItems] = useState([]);
 * const [totalPrice, setTotalPrice] = useState(0);
 * const [discountCode, setDiscountCode] = useState('');
 * const [discountAmount, setDiscountAmount] = useState(0);
 *
 * Pros:
 * - Đơn giản với state nhỏ
 * - Mỗi state độc lập
 *
 * Cons:
 * - Phải tính totalPrice manually mỗi lần items thay đổi
 * - Updates rải rác, dễ sót
 * - Khó sync giữa items và totalPrice
 *
 * Approach B: useReducer
 * const [state, dispatch] = useReducer(cartReducer, initialState);
 *
 * Pros:
 * - State updates tập trung
 * - totalPrice tự động update trong reducer
 * - Dễ test, dễ debug
 * - Predictable state transitions
 *
 * Cons:
 * - Boilerplate code nhiều hơn
 * - Cần hiểu reducer pattern
 *
 * 💭 BẠN CHỌN GÌ VÀ TẠI SAO?
 * (Viết 3-5 câu giải thích quyết định)
 *
 * Sau đó implement approach bạn chọn với:
 * - Reducer function (nếu chọn useReducer)
 * - Component với 3 actions: ADD_ITEM, REMOVE_ITEM, CLEAR_CART
 * - Display total price
 */

// TODO: Viết giải thích + implementation
💡 Solution
jsx
/**
 * Shopping Cart component - Decision: use useReducer
 * Lý do chọn useReducer thay vì multiple useState:
 *
 * 1. State có nhiều phần liên quan chặt chẽ: items → ảnh hưởng trực tiếp đến totalPrice
 * 2. Cần tính toán totalPrice mỗi khi items thay đổi (ADD, REMOVE, UPDATE_QUANTITY)
 * 3. Nhiều hành động phức tạp (APPLY_DISCOUNT, CLEAR_CART) cần cập nhật nhiều field cùng lúc
 * 4. Logic tính toán và validation nên được tập trung trong reducer → dễ test, dễ debug
 * 5. Khi app mở rộng (thêm tax, shipping, coupons, persistence), useReducer dễ mở rộng hơn
 *
 * Trade-off: boilerplate nhiều hơn, nhưng maintainability cao hơn rất nhiều
 */
import { useReducer, useState } from 'react';

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

      let newItems;
      if (existingItem) {
        // Tăng quantity nếu đã có
        newItems = state.items.map((item) =>
          item.id === action.payload.id
            ? { ...item, quantity: item.quantity + 1 }
            : item,
        );
      } else {
        // Thêm mới
        newItems = [...state.items, { ...action.payload, quantity: 1 }];
      }

      return {
        ...state,
        items: newItems,
        totalPrice: calculateTotal(newItems, state.discountAmount),
      };
    }

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

    case 'UPDATE_QUANTITY': {
      const { id, quantity } = action.payload;
      if (quantity < 1) return state; // Không cho quantity < 1

      const newItems = state.items.map((item) =>
        item.id === id ? { ...item, quantity } : item,
      );

      return {
        ...state,
        items: newItems,
        totalPrice: calculateTotal(newItems, state.discountAmount),
      };
    }

    case 'APPLY_DISCOUNT': {
      const discountAmount = action.payload.amount; // giả sử đã validate
      return {
        ...state,
        discountCode: action.payload.code,
        discountAmount,
        totalPrice: calculateTotal(state.items, discountAmount),
      };
    }

    case 'CLEAR_CART':
      return {
        ...state,
        items: [],
        totalPrice: 0,
        discountCode: '',
        discountAmount: 0,
      };

    default:
      throw new Error(`Unknown action type: ${action.type}`);
  }
}

// Helper function - tính tổng tiền (có thể tách ra custom hook sau này)
function calculateTotal(items, discountAmount) {
  const subtotal = items.reduce(
    (sum, item) => sum + item.price * item.quantity,
    0,
  );
  return Math.max(0, subtotal - discountAmount);
}

// ================= COMPONENT =================
function ShoppingCart() {
  const initialState = {
    items: [],
    totalPrice: 0,
    discountCode: '',
    discountAmount: 0,
  };

  const [state, dispatch] = useReducer(cartReducer, initialState);
  const [newItemInput, setNewItemInput] = useState(''); // chỉ dùng tạm để demo

  // Ví dụ item mẫu (trong thực tế sẽ từ API hoặc form)
  const sampleItems = [
    { id: 1, name: 'Laptop', price: 1200 },
    { id: 2, name: 'Mouse', price: 25 },
    { id: 3, name: 'Keyboard', price: 80 },
  ];

  const handleAddSample = (item) => {
    dispatch({ type: 'ADD_ITEM', payload: item });
  };

  const handleRemove = (id) => {
    dispatch({ type: 'REMOVE_ITEM', payload: { id } });
  };

  const handleUpdateQty = (id, quantity) => {
    dispatch({ type: 'UPDATE_QUANTITY', payload: { id, quantity } });
  };

  const handleClear = () => {
    dispatch({ type: 'CLEAR_CART' });
  };

  const handleApplyDiscount = () => {
    // Giả lập discount 10% cho ví dụ
    const subtotal = state.items.reduce(
      (sum, i) => sum + i.price * i.quantity,
      0,
    );
    const discount = Math.round(subtotal * 0.1);
    dispatch({
      type: 'APPLY_DISCOUNT',
      payload: { code: 'SAVE10', amount: discount },
    });
  };

  return (
    <div>
      <h2>Shopping Cart</h2>

      {/* Danh sách sản phẩm mẫu để thêm */}
      <div style={{ marginBottom: '1rem' }}>
        <strong>Add sample items:</strong>{' '}
        {sampleItems.map((item) => (
          <button
            key={item.id}
            onClick={() => handleAddSample(item)}
            style={{ marginRight: '0.5rem' }}
          >
            + {item.name} (${item.price})
          </button>
        ))}
      </div>

      {/* Cart items */}
      {state.items.length === 0 ? (
        <p>Your cart is empty.</p>
      ) : (
        <>
          <ul style={{ listStyle: 'none', padding: 0 }}>
            {state.items.map((item) => (
              <li
                key={item.id}
                style={{
                  display: 'flex',
                  justifyContent: 'space-between',
                  marginBottom: '0.5rem',
                  padding: '0.5rem',
                  borderBottom: '1px solid #eee',
                }}
              >
                <div>
                  {item.name} × {item.quantity}
                </div>
                <div>
                  ${(item.price * item.quantity).toFixed(2)}
                  <button
                    onClick={() => handleUpdateQty(item.id, item.quantity + 1)}
                    style={{ marginLeft: '1rem' }}
                  >
                    +
                  </button>
                  <button
                    onClick={() => handleUpdateQty(item.id, item.quantity - 1)}
                    disabled={item.quantity <= 1}
                    style={{ marginLeft: '0.5rem' }}
                  >
                    -
                  </button>
                  <button
                    onClick={() => handleRemove(item.id)}
                    style={{ marginLeft: '1rem', color: 'red' }}
                  >
                    Remove
                  </button>
                </div>
              </li>
            ))}
          </ul>

          <div style={{ marginTop: '1rem', fontWeight: 'bold' }}>
            Subtotal: $
            {state.items
              .reduce((sum, i) => sum + i.price * i.quantity, 0)
              .toFixed(2)}
          </div>

          {state.discountAmount > 0 && (
            <div style={{ color: 'green' }}>
              Discount ({state.discountCode}): -$
              {state.discountAmount.toFixed(2)}
            </div>
          )}

          <div style={{ fontSize: '1.2rem', marginTop: '0.5rem' }}>
            Total: ${state.totalPrice.toFixed(2)}
          </div>

          <div style={{ marginTop: '1.5rem' }}>
            <button
              onClick={handleApplyDiscount}
              disabled={state.items.length === 0}
            >
              Apply 10% Discount
            </button>
            <button
              onClick={handleClear}
              style={{ marginLeft: '1rem', color: 'red' }}
              disabled={state.items.length === 0}
            >
              Clear Cart
            </button>
          </div>
        </>
      )}
    </div>
  );
}

export default ShoppingCart;

/*
Ví dụ kết quả khi tương tác:
1. Nhấn "+ Laptop" → items: 1 Laptop, total: $1200
2. Nhấn "+ Mouse" → items: Laptop + Mouse, total: $1225
3. Nhấn "+1" bên Laptop → quantity Laptop = 2, total: $2425
4. Nhấn "Apply 10% Discount" → discount $242.5, total: $2182.50
5. Nhấn "Clear Cart" → giỏ hàng trống, total: $0
*/

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

jsx
/**
 * 🎯 Mục tiêu: Build Multi-Step Form với useReducer
 * ⏱️ Thời gian: 40 phút
 *
 * 📋 Product Requirements:
 * User Story: "Là user, tôi muốn điền form đăng ký 3 bước để tạo account"
 *
 * ✅ Acceptance Criteria:
 * - [ ] Step 1: Personal Info (name, email)
 * - [ ] Step 2: Address (street, city, zipcode)
 * - [ ] Step 3: Review & Submit
 * - [ ] Có nút Next/Previous để navigate
 * - [ ] Validate mỗi step trước khi Next
 * - [ ] Hiển thị progress (Step 1/3, 2/3, 3/3)
 * - [ ] Submit ở step cuối
 *
 * 🎨 Technical Constraints:
 * - Dùng useReducer cho state management
 * - State shape: { currentStep, formData, errors, isSubmitting }
 * - Actions: NEXT_STEP, PREV_STEP, UPDATE_FIELD, SET_ERRORS, SUBMIT_START, SUBMIT_SUCCESS
 *
 * 🚨 Edge Cases cần handle:
 * - Không Next nếu có validation errors
 * - Không Previous từ step 1
 * - Không submit nếu đang isSubmitting
 * - Clear errors khi user sửa field
 *
 * 📝 Implementation Checklist:
 * - [ ] Reducer với 6 actions
 * - [ ] Validation function cho mỗi step
 * - [ ] StepIndicator component
 * - [ ] Form fields cho 3 steps
 * - [ ] Navigation buttons với disabled states
 */

// TODO: Implement Multi-Step Form

// Gợi ý State Shape:
const initialState = {
  currentStep: 1,
  formData: {
    // Step 1
    name: '',
    email: '',
    // Step 2
    street: '',
    city: '',
    zipcode: '',
  },
  errors: {},
  isSubmitting: false,
};

// Gợi ý Reducer:
// function multiStepReducer(state, action) {
//   switch (action.type) {
//     case 'NEXT_STEP': ...
//     case 'PREV_STEP': ...
//     case 'UPDATE_FIELD': ...
//     case 'SET_ERRORS': ...
//     case 'SUBMIT_START': ...
//     case 'SUBMIT_SUCCESS': ...
//   }
// }
💡 Solution
jsx
/**
 * Multi-Step Registration Form using useReducer
 * 3 steps: Personal Info → Address → Review & Submit
 * Handles validation per step, navigation, and submission flow
 */
import { useReducer, useState } from 'react';

// ================= STATE & TYPES =================
const initialState = {
  currentStep: 1,
  formData: {
    // Step 1
    name: '',
    email: '',
    // Step 2
    street: '',
    city: '',
    zipcode: '',
  },
  errors: {},
  isSubmitting: false,
};

// ================= VALIDATION =================
const validateStep = (step, values) => {
  const errors = {};

  if (step === 1) {
    if (!values.name.trim()) {
      errors.name = 'Name is required';
    } else if (values.name.trim().length < 2) {
      errors.name = 'Name must be at least 2 characters';
    }

    if (!values.email) {
      errors.email = 'Email is required';
    } else if (!/\S+@\S+\.\S+/.test(values.email)) {
      errors.email = 'Please enter a valid email';
    }
  }

  if (step === 2) {
    if (!values.street.trim()) {
      errors.street = 'Street address is required';
    }
    if (!values.city.trim()) {
      errors.city = 'City is required';
    }
    if (!values.zipcode.trim()) {
      errors.zipcode = 'Zip code is required';
    } else if (!/^\d{5}(-\d{4})?$/.test(values.zipcode)) {
      errors.zipcode =
        'Please enter a valid zip code (e.g. 12345 or 12345-6789)';
    }
  }

  return errors;
};

// ================= REDUCER =================
function multiStepReducer(state, action) {
  switch (action.type) {
    case 'UPDATE_FIELD': {
      const { field, value } = action.payload;
      // Clear error for this field when user types
      const newErrors = { ...state.errors };
      if (newErrors[field]) delete newErrors[field];

      return {
        ...state,
        formData: {
          ...state.formData,
          [field]: value,
        },
        errors: newErrors,
      };
    }

    case 'SET_ERRORS':
      return {
        ...state,
        errors: action.payload.errors,
      };

    case 'NEXT_STEP': {
      const errors = validateStep(state.currentStep, state.formData);
      if (Object.keys(errors).length > 0) {
        return {
          ...state,
          errors,
        };
      }

      if (state.currentStep < 3) {
        return {
          ...state,
          currentStep: state.currentStep + 1,
          errors: {},
        };
      }
      return state;
    }

    case 'PREV_STEP':
      if (state.currentStep > 1) {
        return {
          ...state,
          currentStep: state.currentStep - 1,
          errors: {},
        };
      }
      return state;

    case 'SUBMIT_START':
      if (state.isSubmitting) return state;
      return {
        ...state,
        isSubmitting: true,
        errors: {},
      };

    case 'SUBMIT_SUCCESS':
      return {
        ...initialState,
        currentStep: 4, // success page
      };

    case 'SUBMIT_ERROR':
      return {
        ...state,
        isSubmitting: false,
        errors: { submit: action.payload.message },
      };

    default:
      throw new Error(`Unknown action type: ${action.type}`);
  }
}

// ================= COMPONENT =================
function MultiStepForm() {
  const [state, dispatch] = useReducer(multiStepReducer, initialState);

  const handleChange = (field) => (e) => {
    dispatch({
      type: 'UPDATE_FIELD',
      payload: { field, value: e.target.value },
    });
  };

  const handleNext = () => {
    dispatch({ type: 'NEXT_STEP' });
  };

  const handlePrev = () => {
    dispatch({ type: 'PREV_STEP' });
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    // Final validation (though already checked on step 2 → next)
    const finalErrors = validateStep(2, state.formData);
    if (Object.keys(finalErrors).length > 0) {
      dispatch({ type: 'SET_ERRORS', payload: { errors: finalErrors } });
      return;
    }

    dispatch({ type: 'SUBMIT_START' });

    // Simulate API call
    try {
      await new Promise((resolve) => setTimeout(resolve, 1500));
      // Uncomment to test error:
      // throw new Error('Server is down');

      dispatch({ type: 'SUBMIT_SUCCESS' });
    } catch (err) {
      dispatch({
        type: 'SUBMIT_ERROR',
        payload: { message: err.message || 'Something went wrong' },
      });
    }
  };

  const StepIndicator = () => (
    <div style={{ marginBottom: '2rem', textAlign: 'center' }}>
      <strong>Step {state.currentStep} of 3</strong>
      <div style={{ marginTop: '0.5rem' }}>
        {[1, 2, 3].map((step) => (
          <span
            key={step}
            style={{
              display: 'inline-block',
              width: '30px',
              height: '30px',
              lineHeight: '30px',
              borderRadius: '50%',
              background: state.currentStep >= step ? '#4CAF50' : '#ddd',
              color: 'white',
              margin: '0 8px',
              fontWeight: 'bold',
            }}
          >
            {step}
          </span>
        ))}
      </div>
    </div>
  );

  if (state.currentStep === 4) {
    return (
      <div style={{ textAlign: 'center', padding: '2rem' }}>
        <h2>Registration Successful! 🎉</h2>
        <p>Thank you for signing up.</p>
        <p>Name: {state.formData.name}</p>
        <p>Email: {state.formData.email}</p>
        <p>
          Address: {state.formData.street}, {state.formData.city}{' '}
          {state.formData.zipcode}
        </p>
      </div>
    );
  }

  return (
    <div style={{ maxWidth: '500px', margin: '0 auto', padding: '2rem' }}>
      <h1>Register</h1>
      <StepIndicator />

      <form onSubmit={handleSubmit}>
        {state.currentStep === 1 && (
          <>
            <h3>Personal Information</h3>
            <div style={{ marginBottom: '1rem' }}>
              <label>Name:</label>
              <input
                type='text'
                value={state.formData.name}
                onChange={handleChange('name')}
                placeholder='John Doe'
                style={{ width: '100%', padding: '8px', marginTop: '4px' }}
              />
              {state.errors.name && (
                <span style={{ color: 'red' }}>{state.errors.name}</span>
              )}
            </div>

            <div style={{ marginBottom: '1rem' }}>
              <label>Email:</label>
              <input
                type='email'
                value={state.formData.email}
                onChange={handleChange('email')}
                placeholder='john@example.com'
                style={{ width: '100%', padding: '8px', marginTop: '4px' }}
              />
              {state.errors.email && (
                <span style={{ color: 'red' }}>{state.errors.email}</span>
              )}
            </div>
          </>
        )}

        {state.currentStep === 2 && (
          <>
            <h3>Address</h3>
            <div style={{ marginBottom: '1rem' }}>
              <label>Street:</label>
              <input
                type='text'
                value={state.formData.street}
                onChange={handleChange('street')}
                placeholder='123 Main St'
                style={{ width: '100%', padding: '8px', marginTop: '4px' }}
              />
              {state.errors.street && (
                <span style={{ color: 'red' }}>{state.errors.street}</span>
              )}
            </div>

            <div style={{ marginBottom: '1rem' }}>
              <label>City:</label>
              <input
                type='text'
                value={state.formData.city}
                onChange={handleChange('city')}
                placeholder='New York'
                style={{ width: '100%', padding: '8px', marginTop: '4px' }}
              />
              {state.errors.city && (
                <span style={{ color: 'red' }}>{state.errors.city}</span>
              )}
            </div>

            <div style={{ marginBottom: '1rem' }}>
              <label>Zip Code:</label>
              <input
                type='text'
                value={state.formData.zipcode}
                onChange={handleChange('zipcode')}
                placeholder='10001'
                style={{ width: '100%', padding: '8px', marginTop: '4px' }}
              />
              {state.errors.zipcode && (
                <span style={{ color: 'red' }}>{state.errors.zipcode}</span>
              )}
            </div>
          </>
        )}

        {state.currentStep === 3 && (
          <>
            <h3>Review & Submit</h3>
            <div style={{ marginBottom: '1rem' }}>
              <strong>Name:</strong> {state.formData.name || '—'}
            </div>
            <div style={{ marginBottom: '1rem' }}>
              <strong>Email:</strong> {state.formData.email || '—'}
            </div>
            <div style={{ marginBottom: '1rem' }}>
              <strong>Address:</strong> {state.formData.street || '—'},{' '}
              {state.formData.city || '—'} {state.formData.zipcode || '—'}
            </div>

            {state.errors.submit && (
              <div style={{ color: 'red', margin: '1rem 0' }}>
                {state.errors.submit}
              </div>
            )}
          </>
        )}

        <div
          style={{
            marginTop: '2rem',
            display: 'flex',
            gap: '1rem',
            justifyContent: 'space-between',
          }}
        >
          <button
            type='button'
            onClick={handlePrev}
            disabled={state.currentStep === 1 || state.isSubmitting}
            style={{ padding: '10px 20px', minWidth: '100px' }}
          >
            Previous
          </button>

          {state.currentStep < 3 ? (
            <button
              type='button'
              onClick={handleNext}
              disabled={state.isSubmitting}
              style={{
                padding: '10px 20px',
                minWidth: '100px',
                background: '#4CAF50',
                color: 'white',
              }}
            >
              Next
            </button>
          ) : (
            <button
              type='submit'
              disabled={state.isSubmitting}
              style={{
                padding: '10px 20px',
                minWidth: '120px',
                background: state.isSubmitting ? '#999' : '#2196F3',
                color: 'white',
              }}
            >
              {state.isSubmitting ? 'Submitting...' : 'Submit'}
            </button>
          )}
        </div>
      </form>
    </div>
  );
}

export default MultiStepForm;

/*
Ví dụ kết quả khi tương tác:
1. Step 1 → nhập name & email hợp lệ → Next → Step 2
2. Step 2 → nhập address hợp lệ → Next → Step 3 (Review)
3. Step 3 → thấy toàn bộ thông tin → Submit
   → isSubmitting = true (nút disable)
   → sau 1.5s → Success screen với dữ liệu đã nhập
4. Trường hợp lỗi:
   - Step 1 thiếu name → Next → hiển thị lỗi đỏ, không chuyển step
   - Submit thất bại (nếu throw error) → hiển thị thông báo lỗi
*/

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

jsx
/**
 * 🎯 Mục tiêu: Thiết kế State Management cho Complex Feature
 * ⏱️ Thời gian: 60 phút
 *
 * 🏗️ PHASE 1: Research & Design (20 phút)
 *
 * Context: Bạn đang build Kanban Board (như Trello)
 *
 * Requirements:
 * - 3 columns: Todo, In Progress, Done
 * - Mỗi column có nhiều cards
 * - Drag & drop cards giữa columns
 * - Add/Edit/Delete cards
 * - Filter cards by tag
 * - Search cards by title
 *
 * State Shape Options:
 *
 * Option A: Normalized State
 * {
 *   columns: { 'todo': {...}, 'inProgress': {...}, 'done': {...} },
 *   cards: { 'card1': {...}, 'card2': {...} },
 *   columnOrder: ['todo', 'inProgress', 'done'],
 *   filter: { tag: null, searchText: '' }
 * }
 *
 * Option B: Nested State
 * {
 *   columns: [
 *     { id: 'todo', title: 'Todo', cards: [...] },
 *     { id: 'inProgress', title: 'In Progress', cards: [...] },
 *     { id: 'done', title: 'Done', cards: [...] }
 *   ],
 *   filter: { tag: null, searchText: '' }
 * }
 *
 * Option C: Separate Reducers
 * - columnsReducer
 * - cardsReducer
 * - filterReducer
 * (Combine với combineReducers pattern - tự research)
 *
 * Nhiệm vụ:
 * 1. So sánh 3 approaches
 * 2. Document pros/cons
 * 3. Chọn approach tốt nhất
 * 4. Viết ADR
 *
 * 📝 ADR Template:
 *
 * ## Context
 * Kanban Board cần quản lý columns, cards, filters. Drag & drop yêu cầu
 * cập nhật positions nhanh...
 *
 * ## Decision
 * Chọn Option A: Normalized State
 *
 * ## Rationale
 * - Performance: Update card không cần clone entire column
 * - Flexibility: Dễ reference card từ multiple places
 * - Scalability: Dễ add features (card comments, attachments)
 *
 * ## Consequences
 * Trade-offs:
 * + Fast updates
 * + Easy to find card by ID
 * - More complex queries (need to join data)
 * - Need utility functions to denormalize
 *
 * ## Alternatives Considered
 * - Option B: Simpler but slow for large datasets
 * - Option C: Over-engineering for this scale
 *
 * 💻 PHASE 2: Implementation (30 phút)
 * Implement reducer với approach đã chọn
 * - At least 5 actions: MOVE_CARD, ADD_CARD, DELETE_CARD, UPDATE_FILTER, CLEAR_FILTER
 * - Helper functions nếu cần (denormalize, etc.)
 *
 * 🧪 PHASE 3: Testing (10 phút)
 * Manual test cases:
 * - [ ] Move card from Todo to In Progress
 * - [ ] Move card back
 * - [ ] Delete card updates column
 * - [ ] Filter by tag
 * - [ ] Search by title
 */

// TODO: Viết ADR + Implementation
💡 Solution
jsx
/**
 * Kanban Board Architecture Decision & Implementation using useReducer
 * Level 4: Quyết Định Kiến Trúc - Chọn Normalized State (Option A)
 *
 * ## Context
 * Xây dựng Kanban Board giống Trello với drag & drop, add/edit/delete cards,
 * filter by tag, search by title. Yêu cầu performance tốt khi có nhiều cards,
 * dễ update vị trí khi drag, và dễ mở rộng (comments, attachments, assignees).
 *
 * ## Decision
 * Chọn Option A: Normalized State (flat structure)
 * {
 *   columns: { [columnId]: { id, title, cardIds: string[] } },
 *   cards: { [cardId]: { id, title, description, tags, columnId } },
 *   columnOrder: string[],
 *   filters: { tag: null | string, searchText: '' }
 * }
 *
 * ## Rationale
 * - Performance: Khi drag & drop chỉ cần cập nhật cardIds array của 2 columns → không clone toàn bộ state
 * - Flexibility: Dễ truy xuất card bằng ID từ bất kỳ đâu (filter, search, edit)
 * - Scalability: Dễ thêm fields cho card (comments, dueDate, attachments) mà không làm nested state sâu
 * - Drag & drop: Chỉ cần thay đổi cardIds và columnId → immutable update nhanh
 * - Filter/Search: Duyệt cards object thay vì nested arrays → dễ implement
 *
 * ## Consequences (Trade-offs)
 * + Update nhanh, không cần deep clone
 * + Dễ tìm card theo ID
 * + Dễ serialize cho localStorage hoặc API
 * - Cần helper functions để "denormalize" khi render (kết hợp cards + columns)
 * - Query phức tạp hơn một chút so với nested (nhưng chấp nhận được)
 *
 * ## Alternatives Considered
 * - Option B (Nested): Dễ đọc ban đầu, nhưng drag & drop chậm khi clone arrays lớn
 * - Option C (Separate Reducers): Over-engineering cho feature này, phức tạp hơn cần thiết
 */

/**
 * Kanban Board component with normalized state + useReducer
 */
import { useReducer } from 'react';

// ================= TYPES & INITIAL STATE =================
const initialState = {
  columns: {
    todo: { id: 'todo', title: 'Todo', cardIds: [] },
    inprogress: { id: 'inprogress', title: 'In Progress', cardIds: [] },
    done: { id: 'done', title: 'Done', cardIds: [] },
  },
  cards: {},
  columnOrder: ['todo', 'inprogress', 'done'],
  filters: {
    tag: null, // null = all
    searchText: '',
  },
};

// ================= HELPER FUNCTIONS =================
const denormalizeColumn = (state, columnId) => {
  const column = state.columns[columnId];
  const cards = column.cardIds
    .map((id) => state.cards[id])
    .filter((card) => {
      if (state.filters.searchText) {
        return card.title
          .toLowerCase()
          .includes(state.filters.searchText.toLowerCase());
      }
      if (state.filters.tag) {
        return card.tags?.includes(state.filters.tag);
      }
      return true;
    });
  return { ...column, cards };
};

const getFilteredColumns = (state) => {
  return state.columnOrder.map((colId) => denormalizeColumn(state, colId));
};

// ================= REDUCER =================
function kanbanReducer(state, action) {
  switch (action.type) {
    case 'ADD_CARD': {
      const { title, columnId } = action.payload;
      const cardId = `card-${Date.now()}`;
      const newCard = {
        id: cardId,
        title,
        description: '',
        tags: [],
        columnId,
      };

      return {
        ...state,
        cards: {
          ...state.cards,
          [cardId]: newCard,
        },
        columns: {
          ...state.columns,
          [columnId]: {
            ...state.columns[columnId],
            cardIds: [...state.columns[columnId].cardIds, cardId],
          },
        },
      };
    }

    case 'DELETE_CARD': {
      const { cardId } = action.payload;
      const card = state.cards[cardId];
      if (!card) return state;

      const { [cardId]: removed, ...remainingCards } = state.cards;
      const column = state.columns[card.columnId];
      const newCardIds = column.cardIds.filter((id) => id !== cardId);

      return {
        ...state,
        cards: remainingCards,
        columns: {
          ...state.columns,
          [card.columnId]: {
            ...column,
            cardIds: newCardIds,
          },
        },
      };
    }

    case 'MOVE_CARD': {
      const { cardId, sourceColumnId, targetColumnId, newIndex } =
        action.payload;
      const card = state.cards[cardId];
      if (!card) return state;

      // Remove from source
      const sourceColumn = state.columns[sourceColumnId];
      const sourceCardIds = sourceColumn.cardIds.filter((id) => id !== cardId);

      // Add to target at new position
      const targetColumn = state.columns[targetColumnId];
      const targetCardIds = [...targetColumn.cardIds];
      targetCardIds.splice(newIndex, 0, cardId);

      return {
        ...state,
        cards: {
          ...state.cards,
          [cardId]: { ...card, columnId: targetColumnId },
        },
        columns: {
          ...state.columns,
          [sourceColumnId]: { ...sourceColumn, cardIds: sourceCardIds },
          [targetColumnId]: { ...targetColumn, cardIds: targetCardIds },
        },
      };
    }

    case 'UPDATE_FILTER': {
      return {
        ...state,
        filters: {
          ...state.filters,
          ...action.payload,
        },
      };
    }

    case 'CLEAR_FILTER':
      return {
        ...state,
        filters: { tag: null, searchText: '' },
      };

    default:
      throw new Error(`Unknown action type: ${action.type}`);
  }
}

// ================= COMPONENT =================
function KanbanBoard() {
  const [state, dispatch] = useReducer(kanbanReducer, initialState);

  const filteredColumns = getFilteredColumns(state);

  const handleAddCard = (columnId) => {
    const title = prompt('Enter card title:');
    if (title?.trim()) {
      dispatch({
        type: 'ADD_CARD',
        payload: { title: title.trim(), columnId },
      });
    }
  };

  const handleDeleteCard = (cardId) => {
    if (window.confirm('Delete this card?')) {
      dispatch({ type: 'DELETE_CARD', payload: { cardId } });
    }
  };

  // Drag & Drop handlers (giả lập bằng buttons cho demo)
  const handleMoveCard = (cardId, sourceColumnId, targetColumnId, index) => {
    dispatch({
      type: 'MOVE_CARD',
      payload: { cardId, sourceColumnId, targetColumnId, newIndex: index },
    });
  };

  const handleSearch = (e) => {
    dispatch({
      type: 'UPDATE_FILTER',
      payload: { searchText: e.target.value },
    });
  };

  const handleFilterTag = (tag) => {
    dispatch({
      type: 'UPDATE_FILTER',
      payload: { tag: tag || null },
    });
  };

  return (
    <div style={{ padding: '2rem' }}>
      <h1>Kanban Board</h1>

      {/* Controls */}
      <div
        style={{
          marginBottom: '2rem',
          display: 'flex',
          gap: '1rem',
          alignItems: 'center',
        }}
      >
        <input
          type='text'
          placeholder='Search cards...'
          onChange={handleSearch}
          style={{ padding: '8px', flex: 1, maxWidth: '300px' }}
        />
        <select
          onChange={(e) => handleFilterTag(e.target.value)}
          defaultValue=''
        >
          <option value=''>All Tags</option>
          <option value='urgent'>Urgent</option>
          <option value='feature'>Feature</option>
          <option value='bug'>Bug</option>
        </select>
        <button onClick={() => dispatch({ type: 'CLEAR_FILTER' })}>
          Clear Filters
        </button>
      </div>

      {/* Board */}
      <div style={{ display: 'flex', gap: '1.5rem', overflowX: 'auto' }}>
        {filteredColumns.map((column) => (
          <div
            key={column.id}
            style={{
              background: '#f4f5f7',
              borderRadius: '8px',
              padding: '1rem',
              minWidth: '300px',
            }}
          >
            <h3>
              {column.title} ({column.cards.length})
            </h3>

            <button
              onClick={() => handleAddCard(column.id)}
              style={{ marginBottom: '1rem', width: '100%' }}
            >
              + Add Card
            </button>

            <div style={{ minHeight: '200px' }}>
              {column.cards.map((card, index) => (
                <div
                  key={card.id}
                  style={{
                    background: 'white',
                    padding: '1rem',
                    marginBottom: '0.8rem',
                    borderRadius: '6px',
                    boxShadow: '0 1px 3px rgba(0,0,0,0.1)',
                  }}
                >
                  <strong>{card.title}</strong>
                  <div style={{ marginTop: '0.5rem', fontSize: '0.9rem' }}>
                    Tags: {card.tags?.join(', ') || '—'}
                  </div>
                  <div
                    style={{
                      marginTop: '0.8rem',
                      display: 'flex',
                      gap: '0.5rem',
                    }}
                  >
                    <button
                      onClick={() => handleDeleteCard(card.id)}
                      style={{ color: 'red', fontSize: '0.9rem' }}
                    >
                      Delete
                    </button>
                    {/* Drag & drop demo buttons */}
                    {column.id !== 'todo' && (
                      <button
                        onClick={() =>
                          handleMoveCard(card.id, column.id, 'todo', 0)
                        }
                        style={{ fontSize: '0.9rem' }}
                      >
                        ← To Todo
                      </button>
                    )}
                    {column.id !== 'done' && (
                      <button
                        onClick={() =>
                          handleMoveCard(card.id, column.id, 'done', 0)
                        }
                        style={{ fontSize: '0.9rem' }}
                      >
                        → To Done
                      </button>
                    )}
                  </div>
                </div>
              ))}
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

export default KanbanBoard;

/*
Ví dụ kết quả khi tương tác:
1. Nhấn "+ Add Card" ở Todo → nhập "Implement login" → card xuất hiện ở Todo
2. Nhấn "→ To Done" → card di chuyển sang Done column
3. Gõ "login" vào search → chỉ hiển thị card có "login" trong title
4. Chọn tag "urgent" → chỉ hiển thị cards có tag urgent
5. Nhấn "Delete" → card biến mất, column card count giảm
6. Nhấn "Clear Filters" → trở lại hiển thị tất cả
*/

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

jsx
/**
 * 🎯 Mục tiêu: Build Production-Ready Feature
 * ⏱️ Thời gian: 90 phút
 *
 * 📋 Feature Specification:
 *
 * Build "E-commerce Product Listing với Advanced Filters"
 *
 * Features:
 * 1. Display products grid (from API)
 * 2. Filters:
 *    - Category (Electronics, Clothing, Books)
 *    - Price range (slider)
 *    - Rating (1-5 stars)
 *    - In stock only (checkbox)
 * 3. Sorting:
 *    - Price: Low to High / High to Low
 *    - Rating: High to Low
 *    - Name: A-Z
 * 4. Pagination:
 *    - 12 items per page
 *    - Next/Previous buttons
 * 5. Search by product name
 * 6. URL sync (filters should be in URL query params)
 *
 * 🏗️ Technical Design Doc:
 *
 * 1. Component Architecture:
 *    - ProductListing (container)
 *    - FilterPanel
 *    - ProductGrid
 *    - ProductCard
 *    - Pagination
 *
 * 2. State Management Strategy:
 *    - useReducer cho filter state
 *    - useState cho products data (hoặc useReducer)
 *    - Decision: Tại sao?
 *
 * 3. API Integration:
 *    - Mock API: https://fakestoreapi.com/products
 *    - Client-side filtering (API không support filters)
 *
 * 4. Performance Considerations:
 *    - useMemo cho filtered/sorted products
 *    - Debounce search input
 *    - Lazy load images
 *
 * 5. Error Handling Strategy:
 *    - API errors
 *    - No results state
 *    - Loading states
 *
 * ✅ Production Checklist:
 * - [ ] TypeScript types (optional, nhưng document types)
 * - [ ] Error boundaries (optional, nhưng mention)
 * - [ ] Loading states (skeleton UI)
 * - [ ] Empty states ("No products found")
 * - [ ] Error states ("Failed to load")
 * - [ ] Mobile responsive (basic)
 * - [ ] Accessibility:
 *   - [ ] Keyboard navigation
 *   - [ ] ARIA labels cho filters
 *   - [ ] Focus management
 * - [ ] Performance:
 *   - [ ] Memoize expensive calculations
 *   - [ ] Debounce search
 * - [ ] Code quality:
 *   - [ ] Reducer tests (pseudo-code OK)
 *   - [ ] Helper functions documented
 *   - [ ] Clear naming
 *
 * 📝 Documentation:
 * - README với setup instructions
 * - Reducer actions documentation
 * - State shape documentation
 *
 * 🔍 Code Review Self-Checklist:
 * - [ ] Reducer is pure function
 * - [ ] No missing default case
 * - [ ] Actions have clear names
 * - [ ] State updates are immutable
 * - [ ] Edge cases handled
 * - [ ] Comments cho complex logic
 */

// TODO: Full implementation

// Starter Code:

// Mock API call
const fetchProducts = async () => {
  const response = await fetch('https://fakestoreapi.com/products');
  return response.json();
};

// State Shape (gợi ý):
const initialState = {
  filters: {
    category: 'all',
    priceRange: [0, 1000],
    rating: 0,
    inStock: false,
    searchText: '',
  },
  sort: {
    field: 'name', // 'name' | 'price' | 'rating'
    order: 'asc', // 'asc' | 'desc'
  },
  pagination: {
    currentPage: 1,
    itemsPerPage: 12,
  },
};

// Reducer Actions:
// - SET_CATEGORY
// - SET_PRICE_RANGE
// - SET_RATING
// - TOGGLE_IN_STOCK
// - SET_SEARCH
// - SET_SORT
// - SET_PAGE
// - RESET_FILTERS
💡 Solution
jsx
/**
 * E-commerce Product Listing with Advanced Filters & Pagination
 * Production-ready version using useReducer for filter + sort + pagination state
 * Client-side filtering & sorting from Fake Store API
 */
import { useReducer, useEffect, useState, useMemo, useCallback } from 'react';

// ================= TYPES & INITIAL STATE =================
const initialState = {
  products: [],
  filteredProducts: [],
  filters: {
    category: 'all',
    priceRange: [0, 1000],
    rating: 0,
    inStock: false, // FakeStore has no real stock → simulate with random
    searchText: '',
  },
  sort: {
    field: 'title', // 'title' | 'price' | 'rating'
    order: 'asc', // 'asc' | 'desc'
  },
  pagination: {
    currentPage: 1,
    itemsPerPage: 12,
  },
  loading: true,
  error: null,
};

// ================= REDUCER =================
function productReducer(state, action) {
  switch (action.type) {
    case 'FETCH_START':
      return { ...state, loading: true, error: null };

    case 'FETCH_SUCCESS':
      return {
        ...state,
        products: action.payload,
        loading: false,
        error: null,
      };

    case 'FETCH_ERROR':
      return {
        ...state,
        loading: false,
        error: action.payload,
      };

    case 'SET_CATEGORY':
    case 'SET_PRICE_RANGE':
    case 'SET_RATING':
    case 'TOGGLE_IN_STOCK':
    case 'SET_SEARCH':
    case 'SET_SORT':
    case 'SET_PAGE':
      return {
        ...state,
        [action.payload.key]: action.payload.value,
        pagination:
          action.type === 'SET_PAGE'
            ? { ...state.pagination, currentPage: action.payload.value }
            : state.pagination,
      };

    case 'RESET_FILTERS':
      return {
        ...state,
        filters: initialState.filters,
        sort: initialState.sort,
        pagination: { ...state.pagination, currentPage: 1 },
      };

    default:
      throw new Error(`Unknown action type: ${action.type}`);
  }
}

// ================= HELPER: APPLY FILTERS & SORT =================
const applyFiltersAndSort = (products, filters, sort) => {
  let result = [...products];

  // Search
  if (filters.searchText) {
    const search = filters.searchText.toLowerCase();
    result = result.filter(
      (p) =>
        p.title.toLowerCase().includes(search) ||
        p.description.toLowerCase().includes(search),
    );
  }

  // Category
  if (filters.category !== 'all') {
    result = result.filter((p) => p.category === filters.category);
  }

  // Price range
  result = result.filter(
    (p) => p.price >= filters.priceRange[0] && p.price <= filters.priceRange[1],
  );

  // Rating
  if (filters.rating > 0) {
    result = result.filter((p) => p.rating.rate >= filters.rating);
  }

  // In stock (simulated)
  if (filters.inStock) {
    result = result.filter(() => Math.random() > 0.3); // ~70% in stock
  }

  // Sorting
  result.sort((a, b) => {
    let aVal =
      sort.field === 'title'
        ? a.title
        : sort.field === 'price'
          ? a.price
          : a.rating.rate;
    let bVal =
      sort.field === 'title'
        ? b.title
        : sort.field === 'price'
          ? b.price
          : b.rating.rate;

    if (typeof aVal === 'string') {
      return sort.order === 'asc'
        ? aVal.localeCompare(bVal)
        : bVal.localeCompare(aVal);
    }

    return sort.order === 'asc' ? aVal - bVal : bVal - aVal;
  });

  return result;
};

// ================= COMPONENT =================
function ProductListing() {
  const [state, dispatch] = useReducer(productReducer, initialState);
  const [debouncedSearch, setDebouncedSearch] = useState(
    state.filters.searchText,
  );

  // Fetch products
  useEffect(() => {
    const fetchProducts = async () => {
      dispatch({ type: 'FETCH_START' });
      try {
        const res = await fetch('https://fakestoreapi.com/products');
        if (!res.ok) throw new Error('Failed to fetch products');
        const data = await res.json();
        dispatch({ type: 'FETCH_SUCCESS', payload: data });
      } catch (err) {
        dispatch({ type: 'FETCH_ERROR', payload: err.message });
      }
    };
    fetchProducts();
  }, []);

  // Debounce search
  useEffect(() => {
    const timer = setTimeout(() => {
      dispatch({
        type: 'SET_SEARCH',
        payload: {
          key: 'filters',
          value: { ...state.filters, searchText: debouncedSearch },
        },
      });
    }, 400);

    return () => clearTimeout(timer);
  }, [debouncedSearch]);

  // Computed filtered & sorted products
  const processedProducts = useMemo(() => {
    return applyFiltersAndSort(state.products, state.filters, state.sort);
  }, [state.products, state.filters, state.sort]);

  // Pagination slice
  const paginatedProducts = useMemo(() => {
    const start =
      (state.pagination.currentPage - 1) * state.pagination.itemsPerPage;
    return processedProducts.slice(
      start,
      start + state.pagination.itemsPerPage,
    );
  }, [processedProducts, state.pagination]);

  const totalPages = Math.ceil(
    processedProducts.length / state.pagination.itemsPerPage,
  );

  // Handlers
  const handleCategoryChange = (e) => {
    dispatch({
      type: 'SET_CATEGORY',
      payload: {
        key: 'filters',
        value: { ...state.filters, category: e.target.value },
      },
    });
    dispatch({ type: 'SET_PAGE', payload: { key: 'pagination', value: 1 } });
  };

  const handlePriceChange = (e, bound) => {
    const newRange = [...state.filters.priceRange];
    newRange[bound] = Number(e.target.value);
    dispatch({
      type: 'SET_PRICE_RANGE',
      payload: {
        key: 'filters',
        value: { ...state.filters, priceRange: newRange },
      },
    });
    dispatch({ type: 'SET_PAGE', payload: { key: 'pagination', value: 1 } });
  };

  const handleRatingChange = (e) => {
    dispatch({
      type: 'SET_RATING',
      payload: {
        key: 'filters',
        value: { ...state.filters, rating: Number(e.target.value) },
      },
    });
    dispatch({ type: 'SET_PAGE', payload: { key: 'pagination', value: 1 } });
  };

  const toggleInStock = () => {
    dispatch({
      type: 'TOGGLE_IN_STOCK',
      payload: {
        key: 'filters',
        value: { ...state.filters, inStock: !state.filters.inStock },
      },
    });
    dispatch({ type: 'SET_PAGE', payload: { key: 'pagination', value: 1 } });
  };

  const handleSearchChange = (e) => {
    setDebouncedSearch(e.target.value);
  };

  const handleSortChange = (field) => {
    const newSort = {
      field,
      order:
        state.sort.field === field && state.sort.order === 'asc'
          ? 'desc'
          : 'asc',
    };
    dispatch({
      type: 'SET_SORT',
      payload: { key: 'sort', value: newSort },
    });
  };

  const goToPage = (page) => {
    if (page >= 1 && page <= totalPages) {
      dispatch({
        type: 'SET_PAGE',
        payload: { key: 'pagination', value: page },
      });
    }
  };

  if (state.loading)
    return (
      <div style={{ textAlign: 'center', padding: '4rem' }}>
        Loading products...
      </div>
    );
  if (state.error)
    return (
      <div style={{ color: 'red', textAlign: 'center', padding: '4rem' }}>
        Error: {state.error}
      </div>
    );

  return (
    <div style={{ padding: '2rem', maxWidth: '1400px', margin: '0 auto' }}>
      <h1>Products</h1>

      {/* Filters Panel */}
      <div
        style={{
          display: 'grid',
          gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))',
          gap: '1.5rem',
          marginBottom: '2rem',
          background: '#f9f9f9',
          padding: '1.5rem',
          borderRadius: '8px',
        }}
      >
        <div>
          <label>Search</label>
          <input
            type='text'
            value={debouncedSearch}
            onChange={handleSearchChange}
            placeholder='Search by name or description...'
            style={{ width: '100%', padding: '8px', marginTop: '4px' }}
          />
        </div>

        <div>
          <label>Category</label>
          <select
            value={state.filters.category}
            onChange={handleCategoryChange}
            style={{ width: '100%', padding: '8px', marginTop: '4px' }}
          >
            <option value='all'>All Categories</option>
            <option value="men's clothing">Men's Clothing</option>
            <option value="women's clothing">Women's Clothing</option>
            <option value='jewelery'>Jewelry</option>
            <option value='electronics'>Electronics</option>
          </select>
        </div>

        <div>
          <label>Min Price: ${state.filters.priceRange[0]}</label>
          <input
            type='range'
            min='0'
            max='1000'
            value={state.filters.priceRange[0]}
            onChange={(e) => handlePriceChange(e, 0)}
            style={{ width: '100%' }}
          />
        </div>

        <div>
          <label>Max Price: ${state.filters.priceRange[1]}</label>
          <input
            type='range'
            min='0'
            max='1000'
            value={state.filters.priceRange[1]}
            onChange={(e) => handlePriceChange(e, 1)}
            style={{ width: '100%' }}
          />
        </div>

        <div>
          <label>Minimum Rating</label>
          <select
            value={state.filters.rating}
            onChange={handleRatingChange}
            style={{ width: '100%', padding: '8px', marginTop: '4px' }}
          >
            <option value={0}>Any</option>
            {[1, 2, 3, 4].map((r) => (
              <option
                key={r}
                value={r}
              >
                {r}+ Stars
              </option>
            ))}
          </select>
        </div>

        <div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
          <input
            type='checkbox'
            checked={state.filters.inStock}
            onChange={toggleInStock}
            id='instock'
          />
          <label htmlFor='instock'>In Stock Only (simulated)</label>
        </div>

        <div style={{ gridColumn: '1 / -1', textAlign: 'right' }}>
          <button
            onClick={() => dispatch({ type: 'RESET_FILTERS' })}
            style={{
              padding: '8px 16px',
              background: '#e74c3c',
              color: 'white',
              border: 'none',
              borderRadius: '4px',
            }}
          >
            Reset Filters
          </button>
        </div>
      </div>

      {/* Sort Controls */}
      <div
        style={{
          marginBottom: '1.5rem',
          display: 'flex',
          gap: '1rem',
          flexWrap: 'wrap',
        }}
      >
        <strong>Sort by:</strong>
        {['title', 'price', 'rating'].map((field) => (
          <button
            key={field}
            onClick={() => handleSortChange(field)}
            style={{
              padding: '6px 12px',
              background: state.sort.field === field ? '#3498db' : '#ecf0f1',
              color: state.sort.field === field ? 'white' : 'black',
              border: 'none',
              borderRadius: '4px',
              cursor: 'pointer',
            }}
          >
            {field.charAt(0).toUpperCase() + field.slice(1)}
            {state.sort.field === field &&
              (state.sort.order === 'asc' ? ' ↑' : ' ↓')}
          </button>
        ))}
      </div>

      {/* Products Grid */}
      {paginatedProducts.length === 0 ? (
        <div
          style={{ textAlign: 'center', padding: '4rem', fontSize: '1.2rem' }}
        >
          No products match your filters.
        </div>
      ) : (
        <div
          style={{
            display: 'grid',
            gridTemplateColumns: 'repeat(auto-fill, minmax(250px, 1fr))',
            gap: '1.5rem',
          }}
        >
          {paginatedProducts.map((product) => (
            <div
              key={product.id}
              style={{
                border: '1px solid #ddd',
                borderRadius: '8px',
                overflow: 'hidden',
                background: 'white',
                boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
              }}
            >
              <img
                src={product.image}
                alt={product.title}
                style={{
                  width: '100%',
                  height: '220px',
                  objectFit: 'contain',
                  padding: '1rem',
                }}
                loading='lazy'
              />
              <div style={{ padding: '1rem' }}>
                <h3 style={{ fontSize: '1.1rem', margin: '0 0 0.5rem' }}>
                  {product.title}
                </h3>
                <div
                  style={{
                    fontWeight: 'bold',
                    color: '#e67e22',
                    marginBottom: '0.5rem',
                  }}
                >
                  ${product.price.toFixed(2)}
                </div>
                <div style={{ fontSize: '0.9rem', color: '#7f8c8d' }}>
                  Rating: {product.rating.rate} ★ ({product.rating.count})
                </div>
                <div
                  style={{
                    fontSize: '0.85rem',
                    marginTop: '0.5rem',
                    color: '#95a5a6',
                  }}
                >
                  {product.category}
                </div>
              </div>
            </div>
          ))}
        </div>
      )}

      {/* Pagination */}
      {totalPages > 1 && (
        <div
          style={{
            marginTop: '2rem',
            display: 'flex',
            justifyContent: 'center',
            gap: '0.5rem',
            flexWrap: 'wrap',
          }}
        >
          <button
            onClick={() => goToPage(state.pagination.currentPage - 1)}
            disabled={state.pagination.currentPage === 1}
            style={{ padding: '8px 16px', minWidth: '80px' }}
          >
            Previous
          </button>

          {[...Array(totalPages)].map((_, i) => {
            const page = i + 1;
            return (
              <button
                key={page}
                onClick={() => goToPage(page)}
                style={{
                  padding: '8px 12px',
                  background:
                    state.pagination.currentPage === page
                      ? '#3498db'
                      : '#ecf0f1',
                  color:
                    state.pagination.currentPage === page ? 'white' : 'black',
                  border: 'none',
                  borderRadius: '4px',
                  minWidth: '40px',
                }}
              >
                {page}
              </button>
            );
          })}

          <button
            onClick={() => goToPage(state.pagination.currentPage + 1)}
            disabled={state.pagination.currentPage === totalPages}
            style={{ padding: '8px 16px', minWidth: '80px' }}
          >
            Next
          </button>
        </div>
      )}

      {/* Results count */}
      <div style={{ textAlign: 'center', marginTop: '1rem', color: '#7f8c8d' }}>
        Showing {paginatedProducts.length} of {processedProducts.length}{' '}
        products
      </div>
    </div>
  );
}

export default ProductListing;

/*
Ví dụ kết quả khi tương tác:
1. Trang load → hiển thị 12 sản phẩm đầu tiên
2. Gõ "ring" vào search → chỉ hiển thị sản phẩm có "ring" trong title/description
3. Chọn category "jewelery" → chỉ jewelry, search vẫn hoạt động
4. Kéo min price lên 100 → lọc sản phẩm >= $100
5. Chọn min rating 4 → chỉ sản phẩm >= 4 sao
6. Nhấn sort "Price" → sắp xếp tăng dần, nhấn lại → giảm dần
7. Chuyển sang page 2 → hiển thị sản phẩm 13-24
8. Nhấn Reset Filters → trở về trạng thái ban đầu
*/

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

Bảng So Sánh Trade-offs: useState vs useReducer

Tiêu chíuseStateuseReducerWinner
Setup Complexity✅ Đơn giản, 1 dòng❌ Cần reducer function + actionsuseState
Code Length✅ Ít code hơn❌ Nhiều boilerplateuseState
State Complexity❌ Khó quản lý khi >3 related states✅ Tốt với complex stateuseReducer
Logic Centralization❌ Logic rải rác trong handlers✅ Logic tập trung trong reduceruseReducer
Testability❌ Phải test component✅ Test reducer như pure functionuseReducer
Debugging❌ Khó track "what happened"✅ Log actions = clear historyuseReducer
Performance✅ Nhẹ hơn (ít overhead)⚠️ Overhead nhỏ (thường không đáng kể)useState
Learning Curve✅ Dễ học❌ Cần hiểu reducer patternuseState
Type Safety⚠️ Dễ typo setter names✅ Action types dễ type-checkuseReducer
Refactoring❌ Khó refactor khi app grows✅ Dễ add actions mớiuseReducer

Decision Tree: Khi nào dùng cái nào?

START: Cần quản lý state?

├─ State đơn giản (1-2 primitives)?
│  └─ YES → useState ✅
│     Example: toggle, counter, input value

├─ State là object nhỏ, không có logic phức tạp?
│  └─ YES → useState ✅
│     Example: { isOpen: false, selectedId: null }

├─ State updates phụ thuộc vào current state?
│  └─ YES
│     │
│     ├─ Chỉ 1-2 dependencies?
│     │  └─ useState với functional updates ✅
│     │     Example: setCount(prev => prev + 1)
│     │
│     └─ Multiple complex dependencies?
│        └─ useReducer ✅
│           Example: Form với nhiều fields + validation

├─ Cần update nhiều related states cùng lúc?
│  └─ YES → useReducer ✅
│     Example: Fetch data → update loading, data, error

├─ State transitions có logic rõ ràng (state machine)?
│  └─ YES → useReducer ✅
│     Example: Todo: idle → loading → success/error

├─ Cần dễ test logic riêng biệt khỏi UI?
│  └─ YES → useReducer ✅
│     Test reducer function độc lập

├─ Team đã quen với Redux pattern?
│  └─ YES → useReducer ✅
│     Familiar pattern, easy onboarding

└─ NOT SURE?
   └─ Start with useState
      If it gets messy → Refactor to useReducer

Real-World Use Cases

✅ useState - Perfect for:

  1. Toggle/Boolean states:
jsx
const [isOpen, setIsOpen] = useState(false);
  1. Simple input fields:
jsx
const [email, setEmail] = useState('');
  1. Independent states:
jsx
const [count, setCount] = useState(0);
const [name, setName] = useState('');

✅ useReducer - Perfect for:

  1. Forms với nhiều fields:
jsx
// Thay vì 10 useState, dùng 1 useReducer
const [formState, dispatch] = useReducer(formReducer, initialFormState);
  1. Data fetching với loading/error:
jsx
// State: { data, loading, error }
// Actions: FETCH_START, FETCH_SUCCESS, FETCH_ERROR
  1. Shopping cart, todo list, etc:
jsx
// Complex state với nhiều operations
// Mỗi operation = 1 action
  1. State machines:
jsx
// idle → loading → success/error
// Mỗi transition rõ ràng

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

Bug 1: Infinite Re-renders 🐛

jsx
// ❌ CODE BỊ LỖI
function Counter() {
  const [state, dispatch] = useReducer(counterReducer, { count: 0 });

  // 🐛 BUG: Infinite loop!
  dispatch({ type: 'INCREMENT' });

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

❓ Câu hỏi:

  1. Tại sao code trên gây infinite loop?
  2. Làm sao fix?

💡 Giải thích:

  • dispatch trigger re-render
  • Component re-render → dispatch lại được gọi
  • → Re-render lại → infinite loop!

✅ Fix:

jsx
function Counter() {
  const [state, dispatch] = useReducer(counterReducer, { count: 0 });

  // ✅ Chỉ dispatch trong event handler hoặc useEffect
  const handleClick = () => {
    dispatch({ type: 'INCREMENT' });
  };

  return (
    <div>
      {state.count}
      <button onClick={handleClick}>+1</button>
    </div>
  );
}

Bug 2: Mutating State 🐛

jsx
// ❌ CODE BỊ LỖI
function todoReducer(state, action) {
  switch (action.type) {
    case 'ADD_TODO':
      // 🐛 BUG: Mutate state trực tiếp!
      state.todos.push(action.payload);
      return state;

    case 'TOGGLE_TODO':
      // 🐛 BUG: Mutate nested object!
      const todo = state.todos.find((t) => t.id === action.payload.id);
      todo.completed = !todo.completed;
      return state;

    default:
      return state;
  }
}

❓ Câu hỏi:

  1. Tại sao code trên sai?
  2. Hậu quả là gì?
  3. Làm sao fix?

💡 Giải thích:

  • Reducer PHẢI pure function
  • Mutate state → React không detect change → không re-render
  • Hoặc re-render nhưng UI không sync với state

✅ Fix:

jsx
function todoReducer(state, action) {
  switch (action.type) {
    case 'ADD_TODO':
      // ✅ Return new array
      return {
        ...state,
        todos: [...state.todos, action.payload],
      };

    case 'TOGGLE_TODO':
      // ✅ Return new array với new object
      return {
        ...state,
        todos: state.todos.map((todo) =>
          todo.id === action.payload.id
            ? { ...todo, completed: !todo.completed }
            : todo,
        ),
      };

    default:
      return state;
  }
}

Bug 3: Missing Default Case 🐛

jsx
// ❌ CODE BỊ LỖI
function reducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { count: state.count + 1 };
    case 'DECREMENT':
      return { count: state.count - 1 };
    // 🐛 BUG: No default case!
  }
}

// Sử dụng:
dispatch({ type: 'RESET' }); // Typo: should be 'RESET'
// → Không throw error, silent fail!
// → Trả về undefined → App crash

❓ Câu hỏi:

  1. Tại sao cần default case?
  2. Nên làm gì trong default case?

💡 Giải thích:

  • Typo action type → silent fail
  • Return undefined → React error "Cannot read property of undefined"
  • Khó debug vì không biết action nào gây lỗi

✅ Fix:

jsx
function reducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { count: state.count + 1 };
    case 'DECREMENT':
      return { count: state.count - 1 };

    // ✅ Luôn có default case
    default:
      // Throw error rõ ràng
      throw new Error(`Unknown action type: ${action.type}`);

    // HOẶC log warning và return state
    // console.warn(`Unknown action: ${action.type}`);
    // return state;
  }
}

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

Knowledge Check

Đánh dấu ✅ khi bạn tự tin với concept:

Reducer Basics:

  • [ ] Tôi hiểu reducer pattern là gì
  • [ ] Tôi biết cách viết reducer function
  • [ ] Tôi hiểu reducer phải pure (no side effects, no mutations)
  • [ ] Tôi biết cách định nghĩa actions

useReducer Hook:

  • [ ] Tôi biết syntax: const [state, dispatch] = useReducer(reducer, initialState)
  • [ ] Tôi hiểu dispatch là gì và cách dùng
  • [ ] Tôi biết khi nào dùng useState vs useReducer
  • [ ] Tôi có thể refactor từ useState sang useReducer

Best Practices:

  • [ ] Tôi luôn có default case trong reducer
  • [ ] Tôi không mutate state trong reducer
  • [ ] Tôi đặt tên actions rõ ràng (SCREAMING_SNAKE_CASE)
  • [ ] Tôi hiểu trade-offs của useReducer

Edge Cases:

  • [ ] Tôi biết cách handle validation errors
  • [ ] Tôi biết cách prevent double dispatch (isLoading check)
  • [ ] Tôi biết cách reset state về initial state

Code Review Checklist

Review code của bạn:

Reducer Function:

  • [ ] Pure function (không side effects)
  • [ ] Immutable updates (không mutate state)
  • [ ] Có default case throw error
  • [ ] Action types rõ ràng, consistent naming

State Shape:

  • [ ] Flat khi có thể (avoid deep nesting)
  • [ ] Grouped related data
  • [ ] Không có redundant data (derive khi cần)

Actions:

  • [ ] Type names descriptive
  • [ ] Payload structure consistent
  • [ ] Document payload shape (comments hoặc TS)

Component:

  • [ ] Dispatch trong event handlers/useEffect (không trong render)
  • [ ] Error handling
  • [ ] Loading states
  • [ ] Edge cases covered

🏠 BÀI TẬP VỀ NHÀ

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

Bài 1: Refactor useState to useReducer

Cho component sau dùng useState:

jsx
function Calculator() {
  const [display, setDisplay] = useState('0');
  const [operator, setOperator] = useState(null);
  const [previousValue, setPreviousValue] = useState(null);
  const [waitingForOperand, setWaitingForOperand] = useState(false);

  const inputDigit = (digit) => {
    if (waitingForOperand) {
      setDisplay(String(digit));
      setWaitingForOperand(false);
    } else {
      setDisplay(display === '0' ? String(digit) : display + digit);
    }
  };

  const performOperation = (nextOperator) => {
    const inputValue = parseFloat(display);

    if (previousValue === null) {
      setPreviousValue(inputValue);
    } else if (operator) {
      const currentValue = previousValue || 0;
      const newValue = performCalculation[operator](currentValue, inputValue);

      setDisplay(String(newValue));
      setPreviousValue(newValue);
    }

    setWaitingForOperand(true);
    setOperator(nextOperator);
  };

  // ... rest of logic
}

Nhiệm vụ:

  1. Chuyển sang useReducer
  2. Định nghĩa actions: INPUT_DIGIT, SET_OPERATOR, CALCULATE, CLEAR
  3. Viết reducer handle tất cả logic
💡 Solution
jsx
/**
 * Calculator component refactored from multiple useState to useReducer
 * Manages calculator state with actions: INPUT_DIGIT, INPUT_DECIMAL, SET_OPERATOR, CALCULATE, CLEAR, DELETE
 */
import { useReducer } from 'react';

// ================= REDUCER =================
function calculatorReducer(state, action) {
  switch (action.type) {
    case 'INPUT_DIGIT': {
      const { digit } = action.payload;

      if (state.waitingForOperand) {
        return {
          ...state,
          display: String(digit),
          waitingForOperand: false,
        };
      }

      if (state.display === '0') {
        return {
          ...state,
          display: String(digit),
        };
      }

      return {
        ...state,
        display: state.display + digit,
      };
    }

    case 'INPUT_DECIMAL': {
      if (state.waitingForOperand) {
        return {
          ...state,
          display: '0.',
          waitingForOperand: false,
        };
      }

      if (!state.display.includes('.')) {
        return {
          ...state,
          display: state.display + '.',
        };
      }

      return state; // already has decimal
    }

    case 'SET_OPERATOR': {
      const { operator } = action.payload;
      const inputValue = parseFloat(state.display);

      if (state.previousValue === null) {
        // First operand
        return {
          ...state,
          previousValue: inputValue,
          operator,
          waitingForOperand: true,
        };
      }

      if (state.operator && !state.waitingForOperand) {
        // Chain operations
        const currentValue = state.previousValue || 0;
        const newValue = performCalculation[state.operator](
          currentValue,
          inputValue,
        );

        return {
          ...state,
          display: String(newValue),
          previousValue: newValue,
          operator,
          waitingForOperand: true,
        };
      }

      return {
        ...state,
        operator,
        waitingForOperand: true,
      };
    }

    case 'CALCULATE': {
      if (state.previousValue === null || state.operator === null) {
        return state;
      }

      const inputValue = parseFloat(state.display);
      const currentValue = state.previousValue || 0;
      const newValue = performCalculation[state.operator](
        currentValue,
        inputValue,
      );

      return {
        ...state,
        display: String(newValue),
        previousValue: null,
        operator: null,
        waitingForOperand: true,
      };
    }

    case 'CLEAR':
      return {
        display: '0',
        operator: null,
        previousValue: null,
        waitingForOperand: false,
      };

    case 'DELETE': {
      if (state.waitingForOperand) return state;

      if (state.display.length <= 1) {
        return {
          ...state,
          display: '0',
        };
      }

      return {
        ...state,
        display: state.display.slice(0, -1),
      };
    }

    default:
      throw new Error(`Unknown action type: ${action.type}`);
  }
}

// Calculation functions
const performCalculation = {
  '/': (prev, next) => prev / next,
  '*': (prev, next) => prev * next,
  '+': (prev, next) => prev + next,
  '-': (prev, next) => prev - next,
};

// ================= COMPONENT =================
function Calculator() {
  const initialState = {
    display: '0',
    operator: null,
    previousValue: null,
    waitingForOperand: false,
  };

  const [state, dispatch] = useReducer(calculatorReducer, initialState);

  const handleDigit = (digit) => {
    dispatch({ type: 'INPUT_DIGIT', payload: { digit } });
  };

  const handleDecimal = () => {
    dispatch({ type: 'INPUT_DECIMAL' });
  };

  const handleOperator = (op) => {
    dispatch({ type: 'SET_OPERATOR', payload: { operator: op } });
  };

  const handleEquals = () => {
    dispatch({ type: 'CALCULATE' });
  };

  const handleClear = () => {
    dispatch({ type: 'CLEAR' });
  };

  const handleDelete = () => {
    dispatch({ type: 'DELETE' });
  };

  return (
    <div
      style={{
        maxWidth: '320px',
        margin: '2rem auto',
        border: '1px solid #ccc',
        borderRadius: '12px',
        overflow: 'hidden',
        background: '#f8f9fa',
      }}
    >
      {/* Display */}
      <div
        style={{
          background: '#333',
          color: 'white',
          fontSize: '2.8rem',
          textAlign: 'right',
          padding: '1.5rem 1rem',
          minHeight: '80px',
          fontFamily: 'monospace',
        }}
      >
        {state.display}
      </div>

      {/* Buttons */}
      <div
        style={{
          display: 'grid',
          gridTemplateColumns: 'repeat(4, 1fr)',
          gap: '1px',
          background: '#ddd',
        }}
      >
        <button
          onClick={handleClear}
          style={buttonStyle('orange')}
        >
          C
        </button>
        <button
          onClick={handleDelete}
          style={buttonStyle('gray')}
        >

        </button>
        <button
          onClick={() => handleOperator('/')}
          style={buttonStyle('gray')}
        >
          /
        </button>

        <button
          onClick={() => handleDigit(7)}
          style={buttonStyle()}
        >
          7
        </button>
        <button
          onClick={() => handleDigit(8)}
          style={buttonStyle()}
        >
          8
        </button>
        <button
          onClick={() => handleDigit(9)}
          style={buttonStyle()}
        >
          9
        </button>
        <button
          onClick={() => handleOperator('*')}
          style={buttonStyle('gray')}
        >
          ×
        </button>

        <button
          onClick={() => handleDigit(4)}
          style={buttonStyle()}
        >
          4
        </button>
        <button
          onClick={() => handleDigit(5)}
          style={buttonStyle()}
        >
          5
        </button>
        <button
          onClick={() => handleDigit(6)}
          style={buttonStyle()}
        >
          6
        </button>
        <button
          onClick={() => handleOperator('-')}
          style={buttonStyle('gray')}
        >
          -
        </button>

        <button
          onClick={() => handleDigit(1)}
          style={buttonStyle()}
        >
          1
        </button>
        <button
          onClick={() => handleDigit(2)}
          style={buttonStyle()}
        >
          2
        </button>
        <button
          onClick={() => handleDigit(3)}
          style={buttonStyle()}
        >
          3
        </button>
        <button
          onClick={() => handleOperator('+')}
          style={buttonStyle('gray')}
        >
          +
        </button>

        <button
          onClick={() => handleDigit(0)}
          style={{ ...buttonStyle(), gridColumn: 'span 2' }}
        >
          0
        </button>
        <button
          onClick={handleDecimal}
          style={buttonStyle()}
        >
          .
        </button>
        <button
          onClick={handleEquals}
          style={buttonStyle('orange')}
        >
          =
        </button>
      </div>
    </div>
  );
}

// Helper for consistent button styling
const buttonStyle = (bg = '#fff') => ({
  padding: '1.5rem',
  fontSize: '1.5rem',
  border: 'none',
  background:
    bg === 'orange' ? '#f39c12' : bg === 'gray' ? '#7f8c8d' : '#ecf0f1',
  color: bg === 'orange' || bg === 'gray' ? 'white' : '#2c3e50',
  cursor: 'pointer',
  transition: 'background 0.1s',
  ':hover': { background: bg === 'orange' ? '#e67e22' : '#dfe6e9' },
});

export default Calculator;

/*
Ví dụ kết quả khi tương tác:
1. Nhấn 5 → hiển thị "5"
2. Nhấn + → lưu 5 làm previousValue, operator = '+'
3. Nhấn 3 → hiển thị "3"
4. Nhấn = → tính 5 + 3 = 8, hiển thị "8"
5. Nhấn × → operator = '*', waitingForOperand = true
6. Nhấn 4 → hiển thị "4"
7. Nhấn = → tính 8 × 4 = 32, hiển thị "32"
8. Nhấn C → reset về "0"
9. Nhấn 1 2 3 . 4 5 → hiển thị "123.45"
10. Nhấn ← (delete) → "123.4"
*/

Nâng cao (60 phút)

Bài 2: Build Undo/Redo với useReducer

Requirements:

  1. Tạo drawing canvas đơn giản (grid of clickable cells)
  2. User click cell → toggle color
  3. Có nút Undo (quay lại 1 bước)
  4. Có nút Redo (làm lại bước vừa undo)
  5. Disable Undo khi không có history
  6. Disable Redo khi không có future

State shape gợi ý:

jsx
{
  past: [state1, state2, ...], // Array of previous states
  present: currentState,        // Current state
  future: [state3, state4, ...] // Array of undone states
}

Actions:

  • DRAW (toggle cell color)
  • UNDO
  • REDO
  • CLEAR

Tham khảo pattern: Time Travel Pattern

💡 Solution
jsx
/**
 * Simple Pixel Drawing Canvas with Undo/Redo using useReducer
 * - Grid of clickable cells (10×10)
 * - Click to toggle cell color (black/white)
 * - Undo / Redo buttons with history
 * - Clear button
 */
import { useReducer } from 'react';

// ================= TYPES & INITIAL STATE =================
const GRID_SIZE = 10;

const createEmptyGrid = () =>
  Array(GRID_SIZE)
    .fill()
    .map(() => Array(GRID_SIZE).fill(false));

const initialState = {
  past: [],
  present: createEmptyGrid(),
  future: [],
};

// ================= REDUCER =================
function drawingReducer(state, action) {
  switch (action.type) {
    case 'TOGGLE_CELL': {
      const { row, col } = action.payload;

      // Save current state to past before change
      const newPast = [...state.past, state.present];

      // Create new grid with toggled cell
      const newGrid = state.present.map((r, i) =>
        i === row ? r.map((cell, j) => (j === col ? !cell : cell)) : r,
      );

      return {
        past: newPast,
        present: newGrid,
        future: [], // Clear future when new action occurs
      };
    }

    case 'UNDO': {
      if (state.past.length === 0) return state;

      const previous = state.past[state.past.length - 1];
      const newPast = state.past.slice(0, -1);

      return {
        past: newPast,
        present: previous,
        future: [state.present, ...state.future],
      };
    }

    case 'REDO': {
      if (state.future.length === 0) return state;

      const next = state.future[0];
      const newFuture = state.future.slice(1);

      return {
        past: [...state.past, state.present],
        present: next,
        future: newFuture,
      };
    }

    case 'CLEAR': {
      return {
        past: [],
        present: createEmptyGrid(),
        future: [],
      };
    }

    default:
      throw new Error(`Unknown action type: ${action.type}`);
  }
}

// ================= COMPONENT =================
function DrawingCanvas() {
  const [state, dispatch] = useReducer(drawingReducer, initialState);

  const handleCellClick = (row, col) => {
    dispatch({ type: 'TOGGLE_CELL', payload: { row, col } });
  };

  const handleUndo = () => dispatch({ type: 'UNDO' });
  const handleRedo = () => dispatch({ type: 'REDO' });
  const handleClear = () => dispatch({ type: 'CLEAR' });

  const canUndo = state.past.length > 0;
  const canRedo = state.future.length > 0;

  return (
    <div style={{ textAlign: 'center', padding: '2rem' }}>
      <h2>Pixel Art - Undo/Redo Demo</h2>

      {/* Controls */}
      <div
        style={{
          marginBottom: '1.5rem',
          display: 'flex',
          gap: '1rem',
          justifyContent: 'center',
        }}
      >
        <button
          onClick={handleUndo}
          disabled={!canUndo}
          style={{
            padding: '10px 20px',
            background: canUndo ? '#3498db' : '#bdc3c7',
            color: 'white',
            border: 'none',
            borderRadius: '6px',
            cursor: canUndo ? 'pointer' : 'not-allowed',
          }}
        >
          Undo
        </button>

        <button
          onClick={handleRedo}
          disabled={!canRedo}
          style={{
            padding: '10px 20px',
            background: canRedo ? '#2ecc71' : '#bdc3c7',
            color: 'white',
            border: 'none',
            borderRadius: '6px',
            cursor: canRedo ? 'pointer' : 'not-allowed',
          }}
        >
          Redo
        </button>

        <button
          onClick={handleClear}
          style={{
            padding: '10px 20px',
            background: '#e74c3c',
            color: 'white',
            border: 'none',
            borderRadius: '6px',
          }}
        >
          Clear
        </button>
      </div>

      {/* Canvas Grid */}
      <div
        style={{
          display: 'inline-grid',
          gridTemplateColumns: `repeat(${GRID_SIZE}, 40px)`,
          gridTemplateRows: `repeat(${GRID_SIZE}, 40px)`,
          gap: '2px',
          background: '#ecf0f1',
          padding: '8px',
          borderRadius: '8px',
          boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
        }}
      >
        {state.present.map((row, rowIndex) =>
          row.map((isFilled, colIndex) => (
            <div
              key={`${rowIndex}-${colIndex}`}
              onClick={() => handleCellClick(rowIndex, colIndex)}
              style={{
                width: '40px',
                height: '40px',
                backgroundColor: isFilled ? '#2c3e50' : 'white',
                border: '1px solid #bdc3c7',
                borderRadius: '3px',
                cursor: 'pointer',
                transition: 'background-color 0.12s',
              }}
            />
          )),
        )}
      </div>

      {/* Status */}
      <div style={{ marginTop: '1rem', color: '#7f8c8d' }}>
        History: {state.past.length} past • {state.future.length} future
      </div>
    </div>
  );
}

export default DrawingCanvas;

/*
Ví dụ kết quả khi tương tác:
1. Click vài ô → các ô chuyển thành đen
2. Nhấn Undo → quay lại trạng thái trước đó (các ô trắng trở lại)
3. Nhấn Redo → khôi phục thay đổi vừa undo
4. Vẽ nhiều bước → Undo nhiều lần → Redo nhiều lần
5. Nhấn Clear → canvas trắng, past và future reset
6. Không thể Undo khi past rỗng → nút disabled
7. Không thể Redo khi future rỗng → nút disabled
*/

📚 TÀI LIỆU THAM KHẢO

Bắt buộc đọc

  1. React Docs - useReducer:

  2. When to use useReducer:

Đọc thêm

  1. Redux Style Guide (reducer best practices):

  2. Immer for Immutable Updates:


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

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

  • Ngày 11-12: useState basics + patterns

    • useReducer là "useState on steroids"
    • Cùng purpose: quản lý state
    • Khác approach: centralized vs distributed
  • Ngày 13: Forms với State

    • useReducer làm form logic cleaner
    • 1 reducer thay vì nhiều useState
  • Ngày 14: Lifting State Up

    • useReducer giúp avoid prop drilling
    • Kết hợp Context (Ngày sau) = global state

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

  • Ngày 27: useReducer Advanced Patterns

    • State normalization
    • Reducer composition
    • Async actions pattern
  • Ngày 28: useReducer + useEffect

    • Data fetching với reducer
    • Loading/error states pattern
  • Ngày 29: Custom Hooks với useReducer

    • useAsync, useForm
    • Reusable state machines
  • Ngày 40+: Context + useReducer

    • Global state management
    • Alternative to Redux

💡 SENIOR INSIGHTS

Cân Nhắc Production

1. Khi nào KHÔNG nên dùng useReducer:

jsx
// ❌ Over-engineering: State đơn giản
const [isOpen, setIsOpen] = useState(false);
// Không cần:
// const [state, dispatch] = useReducer(modalReducer, { isOpen: false });

2. Action naming conventions:

jsx
// ✅ GOOD: Descriptive, present tense, SCREAMING_SNAKE_CASE
'ADD_TODO';
'TOGGLE_TODO';
'DELETE_TODO';
'SET_FILTER';

// ❌ BAD: Vague, unclear
'UPDATE'; // Update cái gì?
'change'; // Không consistent casing
'todoToggled'; // Past tense confusing

3. State shape design:

jsx
// ❌ BAD: Deeply nested
{
  user: {
    profile: {
      settings: {
        theme: 'dark'
      }
    }
  }
}

// ✅ GOOD: Flat structure
{
  userTheme: 'dark',
  userProfile: {...},
  userSettings: {...}
}

4. Performance:

useReducer có overhead nhỏ so với useState, nhưng:

  • Thường không đáng kể
  • Trade-off xứng đáng cho code quality
  • Profile before optimizing!

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

Junior Level:

  1. Q: "useState và useReducer khác nhau như thế nào?"

    Expected Answer:

    • useState: Đơn giản, cho state đơn giản
    • useReducer: Phức tạp hơn, cho complex state logic
    • useReducer centralize logic trong reducer
    • Trade-offs: boilerplate vs maintainability
  2. Q: "Reducer function cần tuân thủ quy tắc gì?"

    Expected Answer:

    • Pure function (same input → same output)
    • No side effects
    • Immutable updates
    • Luôn return state (hoặc throw error)

Mid Level:

  1. Q: "Khi nào nên dùng useReducer thay vì useState?"

    Expected Answer:

    • Multiple related state values
    • Next state depends on previous state
    • Complex state logic
    • Easier testing requirements
    • Cần log/debug state transitions
  2. Q: "Làm sao handle async operations với useReducer?"

    Expected Answer:

    • useReducer chỉ handle synchronous updates
    • Async logic trong useEffect hoặc event handlers
    • Dispatch actions: START, SUCCESS, ERROR
    • Pattern sẽ học ở Ngày 28

Senior Level:

  1. Q: "So sánh useReducer với Redux. Khi nào dùng cái nào?"

    Expected Answer:

    • useReducer: Component-level state, đơn giản
    • Redux: Global state, middleware, devtools, time-travel
    • useReducer + Context ≈ mini Redux
    • Start with useReducer, migrate to Redux if needed
    • Trade-off: Simplicity vs Features

War Stories

Story 1: Form Hell → useReducer Heaven

"Tôi từng maintain form đăng ký có 20+ fields với useState. Mỗi lần thêm validation rule, tôi phải update 5-6 places. Bugs xuất hiện liên tục. Khi refactor sang useReducer, tất cả logic ở 1 chỗ. Add validation? Chỉ sửa reducer. Test? Test reducer như function thuần. Dev time giảm 50%." - Senior Engineer

Story 2: Debug với Action Logs

"Production bug: Shopping cart đôi khi mất items. Với useState không biết state thay đổi ở đâu. Sau khi chuyển useReducer, tôi thêm logger middleware log tất cả actions. Phát hiện race condition trong 5 phút. useReducer giúp debugging predictable hơn rất nhiều." - Tech Lead

Story 3: Premature Optimization

"Junior dev trong team nghĩ useReducer 'professional' hơn, refactor TẤT CẢ useState sang useReducer. Kết quả? Code phức tạp không cần thiết, onboarding mới khó khăn. Lesson: Dùng đúng tool cho đúng job. Simple state = useState. Complex state = useReducer." - Engineering Manager


🎯 PREVIEW NGÀY MAI

Ngày 27: useReducer - Advanced Patterns

Bạn sẽ học:

  • ✨ Complex state logic với nested data
  • ✨ State normalization (flat structure)
  • ✨ Reducer composition pattern
  • ✨ Action creators & action types constants
  • ✨ Payload structures best practices

Chuẩn bị:

  • Hoàn thành bài tập hôm nay
  • Review immutable updates (spread, map, filter)
  • Suy nghĩ về cấu trúc state phức tạp

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

Bạn giờ đã hiểu:

  • ✅ Reducer pattern
  • ✅ useReducer hook
  • ✅ Khi nào dùng useState vs useReducer
  • ✅ Best practices & common pitfalls

Keep coding! 💪

Personal tech knowledge base