Skip to content

📅 NGÀY 27: useReducer Advanced Patterns - Normalize State & Action Creators

📍 Vị trí: Phase 3, Tuần 6, Ngày 27/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ẽ:

  • [ ] Thiết kế được normalized state structure cho complex data
  • [ ] Sử dụng được action creators và action type constants
  • [ ] Tổ chức được large reducers với reducer composition
  • [ ] Xử lý được deeply nested state updates immutably
  • [ ] Quyết định được khi nào normalize vs nest state structure

🤔 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. Vấn đề gì xảy ra khi update nested object trong reducer?

    • Gợi ý: state.user.profile.settings.theme = 'dark' - sao sai?
  2. Tại sao action type nên là constant thay vì string literal?

    • Gợi ý: dispatch({ type: 'INCREMET' }) - lỗi gì?
  3. Bạn đã gặp trường hợp reducer quá dài (>200 lines) chưa?

    • Làm sao organize code tốt hơn?

📖 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 Social Media App với state sau:

jsx
// ❌ VẤN ĐỀ: Deeply Nested State
const initialState = {
  users: [
    {
      id: 1,
      name: 'Alice',
      posts: [
        {
          id: 101,
          title: 'Hello World',
          comments: [
            { id: 1001, text: 'Nice post!', author: { id: 2, name: 'Bob' } },
            { id: 1002, text: 'Thanks!', author: { id: 1, name: 'Alice' } },
          ],
        },
      ],
    },
    {
      id: 2,
      name: 'Bob',
      posts: [...],
    },
  ],
};

Thử update 1 comment:

jsx
// 😱 NIGHTMARE: Deeply nested immutable update
function reducer(state, action) {
  switch (action.type) {
    case 'EDIT_COMMENT':
      return {
        ...state,
        users: state.users.map((user) =>
          user.id === action.userId
            ? {
                ...user,
                posts: user.posts.map((post) =>
                  post.id === action.postId
                    ? {
                        ...post,
                        comments: post.comments.map((comment) =>
                          comment.id === action.commentId
                            ? { ...comment, text: action.text }
                            : comment,
                        ),
                      }
                    : post,
                ),
              }
            : user,
        ),
      };
  }
}

Vấn đề:

  1. 🔴 Nested hell - 4 levels deep!
  2. 🔴 Performance - Clone entire tree mỗi lần update 1 comment
  3. 🔴 Error-prone - Dễ quên spread operator
  4. 🔴 Hard to read - Không ai muốn maintain code này
  5. 🔴 Duplication - Data duplicated (author object)

1.2 Giải Pháp: State Normalization

Normalized State = Flat structure, data referenced by IDs

jsx
// ✅ GIẢI PHÁP: Normalized State
const normalizedState = {
  users: {
    1: { id: 1, name: 'Alice', postIds: [101] },
    2: { id: 2, name: 'Bob', postIds: [102] },
  },
  posts: {
    101: {
      id: 101,
      title: 'Hello World',
      authorId: 1,
      commentIds: [1001, 1002],
    },
    102: { id: 102, title: 'My post', authorId: 2, commentIds: [] },
  },
  comments: {
    1001: { id: 1001, text: 'Nice post!', authorId: 2, postId: 101 },
    1002: { id: 1002, text: 'Thanks!', authorId: 1, postId: 101 },
  },
};

Bây giờ update comment:

jsx
// ✅ SIMPLE: Flat update
function reducer(state, action) {
  switch (action.type) {
    case 'EDIT_COMMENT':
      return {
        ...state,
        comments: {
          ...state.comments,
          [action.commentId]: {
            ...state.comments[action.commentId],
            text: action.text,
          },
        },
      };
  }
}

Lợi ích:

  • Flat structure - Không có nested hell
  • Fast updates - Chỉ clone relevant parts
  • No duplication - Reference by ID
  • Easy queries - Direct access: state.comments[id]

1.3 Mental Model

Nested State:

┌─────────────────────────────────────┐
│         DATABASE (Nested)            │
│                                      │
│  User {                              │
│    name: "Alice"                     │
│    posts: [                          │
│      {                               │
│        title: "Hello"                │
│        comments: [                   │
│          { text: "Nice!" }  ← Update này
│        ]                             │
│      }                               │
│    ]                                 │
│  }                                   │
│                                      │
│  → Phải clone: User → Posts → Comments
│  → Slow, error-prone                │
└─────────────────────────────────────┘

Normalized State:

┌─────────────────────────────────────┐
│      DATABASE (Normalized)           │
│                                      │
│  users: { 1: { name: "Alice" } }    │
│                                      │
│  posts: { 101: { title: "Hello" } } │
│                                      │
│  comments: {                         │
│    1001: { text: "Nice!" } ← Update  │
│  }                                   │
│                                      │
│  → Chỉ clone: comments object       │
│  → Fast, predictable                │
└─────────────────────────────────────┘

Analogy: Normalized state giống Database tables với foreign keys

  • Users table → có user IDs
  • Posts table → có post IDs + authorId (foreign key)
  • Comments table → có comment IDs + authorId, postId

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

Hiểu lầm 1: "Normalization luôn tốt hơn nesting"

  • Sự thật: Trade-off! Normalized = fast updates, nhưng queries phức tạp hơn

Hiểu lầm 2: "Phải normalize mọi thứ"

  • Sự thật: Chỉ normalize khi cần. Simple nested data (2 levels) OK!

Hiểu lầm 3: "Action creators là boilerplate không cần thiết"

  • Sự thật: Action creators prevent typos, easy refactoring, type-safe

Hiểu lầm 4: "Reducer composition phức tạp"

  • Sự thật: Pattern đơn giản, giúp code maintainable hơn

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

Demo 1: Action Type Constants & Action Creators ⭐

jsx
// 🎯 PATTERN 1: Action Type Constants

// ❌ CÁCH SAI: Magic strings
function TodoApp() {
  const [state, dispatch] = useReducer(reducer, initialState);

  // 🐛 Typo: 'ADD_TDOO' → Silent fail!
  dispatch({ type: 'ADD_TDOO', payload: { text: 'Learn React' } });
}

// ✅ CÁCH ĐÚNG: Constants
// 1️⃣ Định nghĩa constants
const ActionTypes = {
  ADD_TODO: 'ADD_TODO',
  TOGGLE_TODO: 'TOGGLE_TODO',
  DELETE_TODO: 'DELETE_TODO',
  SET_FILTER: 'SET_FILTER',
};

// 2️⃣ Dùng trong reducer
function todoReducer(state, action) {
  switch (action.type) {
    case ActionTypes.ADD_TODO:
      return {
        ...state,
        todos: [...state.todos, action.payload],
      };

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

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

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

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

// 3️⃣ Dùng trong component
function TodoApp() {
  const [state, dispatch] = useReducer(todoReducer, initialState);

  // ✅ Autocomplete, no typos!
  dispatch({ type: ActionTypes.ADD_TODO, payload: { text: 'Learn React' } });
  //              ↑ IDE sẽ suggest
}

// ------------------------------------------------------------------

// 🎯 PATTERN 2: Action Creators

// ❌ CÁCH SAI: Inline action objects
function TodoApp() {
  const handleAddTodo = (text) => {
    // 🐛 Dễ quên field, typo key names
    dispatch({
      type: ActionTypes.ADD_TODO,
      payload: {
        id: Date.now(),
        text: text,
        completed: false,
        createdAt: new Date().toISOString(),
      },
    });
  };

  // Duplicate logic everywhere!
  const handleAddAnotherTodo = (text) => {
    dispatch({
      type: ActionTypes.ADD_TODO,
      payload: {
        id: Date.now(), // Same logic
        text: text,
        completed: false,
        createdAt: new Date().toISOString(),
      },
    });
  };
}

// ✅ CÁCH ĐÚNG: Action Creators
// 1️⃣ Tạo action creator functions
const actionCreators = {
  addTodo: (text) => ({
    type: ActionTypes.ADD_TODO,
    payload: {
      id: Date.now(),
      text,
      completed: false,
      createdAt: new Date().toISOString(),
    },
  }),

  toggleTodo: (id) => ({
    type: ActionTypes.TOGGLE_TODO,
    payload: { id },
  }),

  deleteTodo: (id) => ({
    type: ActionTypes.DELETE_TODO,
    payload: { id },
  }),

  setFilter: (filter) => ({
    type: ActionTypes.SET_FILTER,
    payload: { filter },
  }),
};

// 2️⃣ Dùng trong component
function TodoApp() {
  const [state, dispatch] = useReducer(todoReducer, initialState);

  const handleAddTodo = (text) => {
    // ✅ Clean, consistent, no duplication
    dispatch(actionCreators.addTodo(text));
  };

  const handleToggle = (id) => {
    dispatch(actionCreators.toggleTodo(id));
  };

  const handleDelete = (id) => {
    dispatch(actionCreators.deleteTodo(id));
  };

  return (
    <div>
      {/* ... */}
      <button onClick={() => handleAddTodo('New todo')}>Add Todo</button>
      {state.todos.map((todo) => (
        <div key={todo.id}>
          <input
            type='checkbox'
            checked={todo.completed}
            onChange={() => handleToggle(todo.id)}
          />
          <span>{todo.text}</span>
          <button onClick={() => handleDelete(todo.id)}>Delete</button>
        </div>
      ))}
    </div>
  );
}

🎯 Lợi ích:

  1. Action Type Constants:

    • ✅ Autocomplete trong IDE
    • ✅ Typo = compile error (với TS) hoặc runtime error ngay
    • ✅ Easy refactoring (rename 1 chỗ)
  2. Action Creators:

    • ✅ Centralized action logic
    • ✅ Consistent payload structure
    • ✅ Easy to test
    • ✅ Reusable across components

Demo 2: State Normalization - Blog App ⭐⭐

jsx
// 🎯 KỊCH BẢN: Blog với Users, Posts, Comments

// ❌ NESTED STATE (Anti-pattern cho large data)
const nestedState = {
  users: [
    {
      id: 1,
      name: 'Alice',
      posts: [
        {
          id: 101,
          title: 'First Post',
          comments: [
            { id: 1001, text: 'Great!', author: { id: 2, name: 'Bob' } },
          ],
        },
      ],
    },
  ],
};

// ✅ NORMALIZED STATE
const normalizedState = {
  entities: {
    users: {
      1: { id: 1, name: 'Alice' },
      2: { id: 2, name: 'Bob' },
    },
    posts: {
      101: { id: 101, title: 'First Post', authorId: 1 },
      102: { id: 102, title: 'Second Post', authorId: 2 },
    },
    comments: {
      1001: { id: 1001, text: 'Great!', authorId: 2, postId: 101 },
      1002: { id: 1002, text: 'Nice!', authorId: 1, postId: 102 },
    },
  },
  // IDs arrays cho ordering
  allUserIds: [1, 2],
  allPostIds: [101, 102],
  postCommentIds: {
    101: [1001],
    102: [1002],
  },
};

// ------------------------------------------------------------------

// 🏗️ ACTION TYPES
const ActionTypes = {
  ADD_POST: 'ADD_POST',
  EDIT_POST: 'EDIT_POST',
  DELETE_POST: 'DELETE_POST',
  ADD_COMMENT: 'ADD_COMMENT',
  EDIT_COMMENT: 'EDIT_COMMENT',
  DELETE_COMMENT: 'DELETE_COMMENT',
};

// 🏭 ACTION CREATORS
const actions = {
  addPost: (authorId, title, content) => ({
    type: ActionTypes.ADD_POST,
    payload: {
      id: Date.now(),
      authorId,
      title,
      content,
      createdAt: new Date().toISOString(),
    },
  }),

  editPost: (postId, updates) => ({
    type: ActionTypes.EDIT_POST,
    payload: { postId, updates },
  }),

  deletePost: (postId) => ({
    type: ActionTypes.DELETE_POST,
    payload: { postId },
  }),

  addComment: (postId, authorId, text) => ({
    type: ActionTypes.ADD_COMMENT,
    payload: {
      id: Date.now(),
      postId,
      authorId,
      text,
      createdAt: new Date().toISOString(),
    },
  }),

  editComment: (commentId, text) => ({
    type: ActionTypes.EDIT_COMMENT,
    payload: { commentId, text },
  }),

  deleteComment: (commentId, postId) => ({
    type: ActionTypes.DELETE_COMMENT,
    payload: { commentId, postId },
  }),
};

// 🔧 REDUCER
function blogReducer(state, action) {
  switch (action.type) {
    case ActionTypes.ADD_POST: {
      const { id, authorId, title, content, createdAt } = action.payload;

      return {
        ...state,
        entities: {
          ...state.entities,
          posts: {
            ...state.entities.posts,
            [id]: { id, authorId, title, content, createdAt },
          },
        },
        allPostIds: [...state.allPostIds, id],
        postCommentIds: {
          ...state.postCommentIds,
          [id]: [],
        },
      };
    }

    case ActionTypes.EDIT_POST: {
      const { postId, updates } = action.payload;

      return {
        ...state,
        entities: {
          ...state.entities,
          posts: {
            ...state.entities.posts,
            [postId]: {
              ...state.entities.posts[postId],
              ...updates,
              updatedAt: new Date().toISOString(),
            },
          },
        },
      };
    }

    case ActionTypes.DELETE_POST: {
      const { postId } = action.payload;

      // 1️⃣ Remove post
      const { [postId]: deletedPost, ...remainingPosts } = state.entities.posts;

      // 2️⃣ Remove from allPostIds
      const newAllPostIds = state.allPostIds.filter((id) => id !== postId);

      // 3️⃣ Remove associated comments
      const commentIdsToDelete = state.postCommentIds[postId] || [];
      const newComments = { ...state.entities.comments };
      commentIdsToDelete.forEach((commentId) => {
        delete newComments[commentId];
      });

      // 4️⃣ Remove postCommentIds entry
      const { [postId]: deletedCommentIds, ...remainingPostCommentIds } =
        state.postCommentIds;

      return {
        ...state,
        entities: {
          ...state.entities,
          posts: remainingPosts,
          comments: newComments,
        },
        allPostIds: newAllPostIds,
        postCommentIds: remainingPostCommentIds,
      };
    }

    case ActionTypes.ADD_COMMENT: {
      const { id, postId, authorId, text, createdAt } = action.payload;

      return {
        ...state,
        entities: {
          ...state.entities,
          comments: {
            ...state.entities.comments,
            [id]: { id, postId, authorId, text, createdAt },
          },
        },
        postCommentIds: {
          ...state.postCommentIds,
          [postId]: [...(state.postCommentIds[postId] || []), id],
        },
      };
    }

    case ActionTypes.EDIT_COMMENT: {
      const { commentId, text } = action.payload;

      return {
        ...state,
        entities: {
          ...state.entities,
          comments: {
            ...state.entities.comments,
            [commentId]: {
              ...state.entities.comments[commentId],
              text,
              updatedAt: new Date().toISOString(),
            },
          },
        },
      };
    }

    case ActionTypes.DELETE_COMMENT: {
      const { commentId, postId } = action.payload;

      // Remove comment from entities
      const { [commentId]: deleted, ...remainingComments } =
        state.entities.comments;

      // Remove from postCommentIds
      const newPostCommentIds = state.postCommentIds[postId].filter(
        (id) => id !== commentId,
      );

      return {
        ...state,
        entities: {
          ...state.entities,
          comments: remainingComments,
        },
        postCommentIds: {
          ...state.postCommentIds,
          [postId]: newPostCommentIds,
        },
      };
    }

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

// 🎯 COMPONENT với Denormalization
function BlogApp() {
  const initialState = {
    entities: {
      users: {
        1: { id: 1, name: 'Alice' },
        2: { id: 2, name: 'Bob' },
      },
      posts: {},
      comments: {},
    },
    allUserIds: [1, 2],
    allPostIds: [],
    postCommentIds: {},
  };

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

  // ✅ Helper: Denormalize data for display
  const getPostWithAuthor = (postId) => {
    const post = state.entities.posts[postId];
    if (!post) return null;

    const author = state.entities.users[post.authorId];
    return {
      ...post,
      author,
    };
  };

  const getCommentsForPost = (postId) => {
    const commentIds = state.postCommentIds[postId] || [];
    return commentIds.map((commentId) => {
      const comment = state.entities.comments[commentId];
      const author = state.entities.users[comment.authorId];
      return {
        ...comment,
        author,
      };
    });
  };

  const handleAddPost = (authorId, title, content) => {
    dispatch(actions.addPost(authorId, title, content));
  };

  const handleAddComment = (postId, authorId, text) => {
    dispatch(actions.addComment(postId, authorId, text));
  };

  return (
    <div>
      <h1>Blog Posts</h1>

      {/* Add Post Form */}
      <button onClick={() => handleAddPost(1, 'New Post', 'Content here')}>
        Add Post
      </button>

      {/* Posts List */}
      {state.allPostIds.map((postId) => {
        const post = getPostWithAuthor(postId);
        const comments = getCommentsForPost(postId);

        return (
          <article key={postId}>
            <h2>{post.title}</h2>
            <p>By {post.author.name}</p>
            <p>{post.content}</p>

            {/* Comments */}
            <div>
              <h3>Comments ({comments.length})</h3>
              {comments.map((comment) => (
                <div key={comment.id}>
                  <strong>{comment.author.name}:</strong> {comment.text}
                  <button
                    onClick={() =>
                      dispatch(actions.editComment(comment.id, 'Updated!'))
                    }
                  >
                    Edit
                  </button>
                  <button
                    onClick={() =>
                      dispatch(actions.deleteComment(comment.id, postId))
                    }
                  >
                    Delete
                  </button>
                </div>
              ))}

              <button onClick={() => handleAddComment(postId, 2, 'Nice post!')}>
                Add Comment
              </button>
            </div>
          </article>
        );
      })}
    </div>
  );
}

🎯 Key Takeaways:

  1. Normalized benefits:

    • ✅ Edit comment: O(1) access, không cần traverse tree
    • ✅ Delete post: Xóa post + cascade delete comments
    • ✅ No data duplication (author referenced by ID)
  2. Trade-off:

    • ➖ Phải denormalize khi display (helper functions)
    • ➕ But update operations fast & simple

Demo 3: Reducer Composition ⭐⭐⭐

jsx
// 🎯 PATTERN: Split large reducer thành smaller reducers

// ❌ VẤN ĐỀ: Reducer quá lớn (300+ lines)
function monolithicReducer(state, action) {
  switch (action.type) {
    case 'USER_LOGIN': /* ... */
    case 'USER_LOGOUT': /* ... */
    case 'UPDATE_PROFILE': /* ... */
    case 'ADD_POST': /* ... */
    case 'EDIT_POST': /* ... */
    case 'DELETE_POST': /* ... */
    case 'ADD_COMMENT': /* ... */
    case 'EDIT_COMMENT': /* ... */
    case 'DELETE_COMMENT': /* ... */
    case 'SET_THEME': /* ... */
    case 'SET_LANGUAGE': /* ... */
    // ... 50 more cases
    default:
      throw new Error(`Unknown action: ${action.type}`);
  }
}

// ✅ GIẢI PHÁP: Reducer Composition
// 1️⃣ Split theo domain (users, posts, comments, settings)

// Users Reducer
function usersReducer(state = {}, action) {
  switch (action.type) {
    case ActionTypes.USER_LOGIN:
      return {
        ...state,
        [action.payload.id]: action.payload,
      };

    case ActionTypes.USER_LOGOUT:
      const { [action.payload.id]: removed, ...remaining } = state;
      return remaining;

    case ActionTypes.UPDATE_PROFILE:
      return {
        ...state,
        [action.payload.id]: {
          ...state[action.payload.id],
          ...action.payload.updates,
        },
      };

    default:
      return state;
  }
}

// Posts Reducer
function postsReducer(state = {}, action) {
  switch (action.type) {
    case ActionTypes.ADD_POST:
      return {
        ...state,
        [action.payload.id]: action.payload,
      };

    case ActionTypes.EDIT_POST:
      return {
        ...state,
        [action.payload.postId]: {
          ...state[action.payload.postId],
          ...action.payload.updates,
        },
      };

    case ActionTypes.DELETE_POST:
      const { [action.payload.postId]: deleted, ...remaining } = state;
      return remaining;

    default:
      return state;
  }
}

// Comments Reducer
function commentsReducer(state = {}, action) {
  switch (action.type) {
    case ActionTypes.ADD_COMMENT:
      return {
        ...state,
        [action.payload.id]: action.payload,
      };

    case ActionTypes.EDIT_COMMENT:
      return {
        ...state,
        [action.payload.commentId]: {
          ...state[action.payload.commentId],
          text: action.payload.text,
        },
      };

    case ActionTypes.DELETE_COMMENT:
      const { [action.payload.commentId]: deleted, ...remaining } = state;
      return remaining;

    // ✅ Cross-domain logic: Delete post → Delete comments
    case ActionTypes.DELETE_POST: {
      const commentIdsToDelete = action.payload.commentIds || [];
      const newState = { ...state };
      commentIdsToDelete.forEach((id) => {
        delete newState[id];
      });
      return newState;
    }

    default:
      return state;
  }
}

// Settings Reducer
function settingsReducer(state = { theme: 'light', language: 'en' }, action) {
  switch (action.type) {
    case ActionTypes.SET_THEME:
      return {
        ...state,
        theme: action.payload.theme,
      };

    case ActionTypes.SET_LANGUAGE:
      return {
        ...state,
        language: action.payload.language,
      };

    default:
      return state;
  }
}

// 2️⃣ Combine reducers manually (NO Redux!)
function rootReducer(state, action) {
  return {
    users: usersReducer(state.users, action),
    posts: postsReducer(state.posts, action),
    comments: commentsReducer(state.comments, action),
    settings: settingsReducer(state.settings, action),
  };
}

// 3️⃣ Sử dụng
function App() {
  const initialState = {
    users: {},
    posts: {},
    comments: {},
    settings: { theme: 'light', language: 'en' },
  };

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

  // ✅ Now dispatch works with composed reducer
  dispatch(actions.addPost(1, 'Title', 'Content'));
  dispatch(actions.setTheme('dark'));

  return (
    <div>
      <p>Theme: {state.settings.theme}</p>
      <p>Posts: {Object.keys(state.posts).length}</p>
    </div>
  );
}

🎯 Lợi ích Reducer Composition:

  1. Maintainability:

    • ✅ Mỗi reducer < 100 lines
    • ✅ Easy to find logic (users logic → usersReducer)
    • ✅ Test mỗi reducer independently
  2. Separation of Concerns:

    • ✅ Each domain isolated
    • ✅ Clear boundaries
  3. Reusability:

    • ✅ Có thể reuse sub-reducers
    • ✅ Share reducers across projects

🔨 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: Convert magic strings → constants + action creators
 * ⏱️ Thời gian: 15 phút
 * 🚫 KHÔNG dùng: Redux, Context
 *
 * Requirements:
 * 1. Tạo ActionTypes object với constants
 * 2. Tạo action creators cho tất cả actions
 * 3. Refactor component dùng action creators
 *
 * 💡 Gợi ý:
 * - ActionTypes = { INCREMENT: 'INCREMENT', ... }
 * - actionCreators = { increment: () => ({ type: ... }), ... }
 */

// ❌ CODE HIỆN TẠI: Magic strings everywhere
function Counter() {
  const reducer = (state, action) => {
    switch (action.type) {
      case 'INCREMENT':
        return { count: state.count + 1 };
      case 'DECREMENT':
        return { count: state.count - 1 };
      case 'INCREMENT_BY':
        return { count: state.count + action.payload };
      case 'RESET':
        return { count: 0 };
      default:
        throw new Error(`Unknown action: ${action.type}`);
    }
  };

  const [state, dispatch] = useReducer(reducer, { count: 0 });

  return (
    <div>
      <h1>{state.count}</h1>
      <button onClick={() => dispatch({ type: 'INCREMENT' })}>+1</button>
      <button onClick={() => dispatch({ type: 'DECREMENT' })}>-1</button>
      <button onClick={() => dispatch({ type: 'INCREMENT_BY', payload: 5 })}>
        +5
      </button>
      <button onClick={() => dispatch({ type: 'RESET' })}>Reset</button>
    </div>
  );
}

// 🎯 NHIỆM VỤ: Refactor với constants + action creators
// TODO: Viết ActionTypes
// TODO: Viết action creators
// TODO: Update component
jsx
/**
 * Counter Component with Action Types & Action Creators
 * @returns {JSX.Element}
 */
function Counter() {
  // ── Action Type Constants ────────────────────────────────────────
  const ActionTypes = {
    INCREMENT: 'INCREMENT',
    DECREMENT: 'DECREMENT',
    INCREMENT_BY: 'INCREMENT_BY',
    RESET: 'RESET',
  };

  // ── Action Creators ──────────────────────────────────────────────
  const actions = {
    increment: () => ({
      type: ActionTypes.INCREMENT,
    }),

    decrement: () => ({
      type: ActionTypes.DECREMENT,
    }),

    incrementBy: (amount) => ({
      type: ActionTypes.INCREMENT_BY,
      payload: amount,
    }),

    reset: () => ({
      type: ActionTypes.RESET,
    }),
  };

  // ── Reducer ──────────────────────────────────────────────────────
  const reducer = (state, action) => {
    switch (action.type) {
      case ActionTypes.INCREMENT:
        return { count: state.count + 1 };

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

      case ActionTypes.INCREMENT_BY:
        return { count: state.count + action.payload };

      case ActionTypes.RESET:
        return { count: 0 };

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

  const [state, dispatch] = React.useReducer(reducer, { count: 0 });

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

      <button onClick={() => dispatch(actions.increment())}>+1</button>
      <button onClick={() => dispatch(actions.decrement())}>-1</button>
      <button onClick={() => dispatch(actions.incrementBy(5))}>+5</button>
      <button onClick={() => dispatch(actions.reset())}>Reset</button>
    </div>
  );
}

/*
Kết quả ví dụ:

Ban đầu: 0
Nhấn +1     → 1
Nhấn +5     → 6
Nhấn -1     → 5
Nhấn Reset  → 0
*/
💡 Solution
jsx
// Đáp án đầy đủ (đã bao gồm cả phần trên)

const ActionTypes = {
  INCREMENT: 'INCREMENT',
  DECREMENT: 'DECREMENT',
  INCREMENT_BY: 'INCREMENT_BY',
  RESET: 'RESET',
};

const actions = {
  increment: () => ({ type: ActionTypes.INCREMENT }),
  decrement: () => ({ type: ActionTypes.DECREMENT }),
  incrementBy: (amount) => ({
    type: ActionTypes.INCREMENT_BY,
    payload: amount,
  }),
  reset: () => ({ type: ActionTypes.RESET }),
};

function reducer(state, action) {
  switch (action.type) {
    case ActionTypes.INCREMENT:
      return { count: state.count + 1 };
    case ActionTypes.DECREMENT:
      return { count: state.count - 1 };
    case ActionTypes.INCREMENT_BY:
      return { count: state.count + action.payload };
    case ActionTypes.RESET:
      return { count: 0 };
    default:
      throw new Error(`Unknown action: ${action.type}`);
  }
}

// Trong component:
const [state, dispatch] = useReducer(reducer, { count: 0 });

// Sử dụng:
<button onClick={() => dispatch(actions.increment())}>+1</button>
<button onClick={() => dispatch(actions.decrement())}>-1</button>
<button onClick={() => dispatch(actions.incrementBy(5))}>+5</button>
<button onClick={() => dispatch(actions.reset())}>Reset</button>

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

jsx
/**
 * 🎯 Mục tiêu: Quyết định Nested vs Normalized State
 * ⏱️ Thời gian: 25 phút
 *
 * Scenario: E-commerce Shopping Cart
 *
 * Products catalog:
 * - 100+ products
 * - Each product có categories, reviews
 * - User có thể add product to cart
 * - Cart hiển thị product details (name, price, image)
 *
 * 🤔 PHÂN TÍCH:
 *
 * Approach A: Nested State
 * {
 *   cart: [
 *     {
 *       product: {
 *         id: 1,
 *         name: 'iPhone',
 *         price: 999,
 *         categories: [...],
 *         reviews: [...]
 *       },
 *       quantity: 2
 *     }
 *   ]
 * }
 *
 * Pros:
 * - Đơn giản, dễ hiểu
 * - Query dễ (cart items có đầy đủ info)
 *
 * Cons:
 * - Duplicate product data (nếu add 2 lần)
 * - Update product price → phải update tất cả cart items
 * - Large payload (categories, reviews không cần trong cart)
 *
 * Approach B: Normalized State
 * {
 *   products: {
 *     1: { id: 1, name: 'iPhone', price: 999, ... }
 *   },
 *   cart: {
 *     items: {
 *       1: { productId: 1, quantity: 2 }
 *     }
 *   }
 * }
 *
 * Pros:
 * - No duplication
 * - Update product → auto reflect in cart
 * - Small cart payload
 *
 * Cons:
 * - Denormalize khi display cart
 * - More complex queries
 *
 * 💭 BẠN CHỌN GÌ VÀ TẠI SAO?
 * (Viết 5-7 câu, consider:)
 * - Number of products (100+)
 * - Update frequency (prices change?)
 * - Display requirements (cart UI cần gì?)
 *
 * Sau đó implement approach đã chọn:
 * - State shape
 * - Reducer với actions: ADD_TO_CART, REMOVE_FROM_CART, UPDATE_QUANTITY
 * - Component display cart
 */

// TODO: Viết analysis + implementation
💡 Solution
jsx
/**
 * E-commerce Shopping Cart với Normalized State
 *
 * Phân tích: Với 100+ products, updates thường xuyên (giá thay đổi),
 * và cần hiển thị cart với info đầy đủ nhưng không muốn duplicate data,
 * normalized state là lựa chọn tốt hơn nested state.
 *
 * Lợi ích:
 * - Không duplicate product info
 * - Update giá product tự động reflect trong cart
 * - Cart state nhẹ (chỉ chứa productId + quantity)
 * - Dễ quản lý khi remove product từ catalog
 *
 * @returns {JSX.Element}
 */
function ShoppingCart() {
  // ── Action Type Constants ────────────────────────────────────────
  const ActionTypes = {
    ADD_TO_CART: 'ADD_TO_CART',
    REMOVE_FROM_CART: 'REMOVE_FROM_CART',
    UPDATE_QUANTITY: 'UPDATE_QUANTITY',
  };

  // ── Action Creators ──────────────────────────────────────────────
  const actions = {
    addToCart: (productId, quantity = 1) => ({
      type: ActionTypes.ADD_TO_CART,
      payload: { productId, quantity },
    }),

    removeFromCart: (productId) => ({
      type: ActionTypes.REMOVE_FROM_CART,
      payload: { productId },
    }),

    updateQuantity: (productId, quantity) => ({
      type: ActionTypes.UPDATE_QUANTITY,
      payload: { productId, quantity },
    }),
  };

  // ── Initial State (Normalized) ───────────────────────────────────
  const initialState = {
    products: {
      1: { id: 1, name: 'iPhone 15', price: 999, category: 'Electronics' },
      2: { id: 2, name: 'MacBook Pro', price: 2499, category: 'Electronics' },
      3: { id: 3, name: 'AirPods', price: 199, category: 'Electronics' },
      4: { id: 4, name: 'React Book', price: 49, category: 'Books' },
    },
    cart: {
      items: {}, // { productId: { productId, quantity } }
    },
  };

  // ── Reducer ──────────────────────────────────────────────────────
  const reducer = (state, action) => {
    switch (action.type) {
      case ActionTypes.ADD_TO_CART: {
        const { productId, quantity } = action.payload;

        // Nếu đã có trong cart, tăng quantity, nếu chưa thì thêm mới
        const currentItem = state.cart.items[productId];
        const newItem = currentItem
          ? { ...currentItem, quantity: currentItem.quantity + quantity }
          : { productId, quantity };

        return {
          ...state,
          cart: {
            ...state.cart,
            items: {
              ...state.cart.items,
              [productId]: newItem,
            },
          },
        };
      }

      case ActionTypes.REMOVE_FROM_CART: {
        const { productId } = action.payload;
        const { [productId]: removed, ...remainingItems } = state.cart.items;

        return {
          ...state,
          cart: {
            ...state.cart,
            items: remainingItems,
          },
        };
      }

      case ActionTypes.UPDATE_QUANTITY: {
        const { productId, quantity } = action.payload;

        if (quantity <= 0) {
          // Nếu quantity <= 0 thì remove khỏi cart
          const { [productId]: removed, ...remainingItems } = state.cart.items;
          return {
            ...state,
            cart: {
              ...state.cart,
              items: remainingItems,
            },
          };
        }

        return {
          ...state,
          cart: {
            ...state.cart,
            items: {
              ...state.cart.items,
              [productId]: {
                ...state.cart.items[productId],
                quantity,
              },
            },
          },
        };
      }

      default:
        return state;
    }
  };

  const [state, dispatch] = React.useReducer(reducer, initialState);

  // ── Helper: Lấy cart items với product details ───────────────────
  const getCartItemsWithDetails = () => {
    return Object.values(state.cart.items).map((cartItem) => {
      const product = state.products[cartItem.productId];
      return {
        ...cartItem,
        ...product, // denormalize: thêm product info vào cart item
        totalPrice: product.price * cartItem.quantity,
      };
    });
  };

  const cartItems = getCartItemsWithDetails();
  const total = cartItems.reduce((sum, item) => sum + item.totalPrice, 0);

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

      {cartItems.length === 0 ? (
        <p>Cart is empty</p>
      ) : (
        <>
          <ul>
            {cartItems.map((item) => (
              <li key={item.productId}>
                <strong>{item.name}</strong>
                <span>
                  {' '}
                  - ${item.price} x {item.quantity}
                </span>
                <span> = ${item.totalPrice}</span>
                <br />
                <button
                  onClick={() =>
                    dispatch(
                      actions.updateQuantity(item.productId, item.quantity + 1),
                    )
                  }
                >
                  +
                </button>
                <button
                  onClick={() =>
                    dispatch(
                      actions.updateQuantity(item.productId, item.quantity - 1),
                    )
                  }
                >
                  -
                </button>
                <button
                  onClick={() =>
                    dispatch(actions.removeFromCart(item.productId))
                  }
                >
                  Remove
                </button>
              </li>
            ))}
          </ul>

          <p>
            <strong>Total: ${total}</strong>
          </p>
        </>
      )}

      {/* Demo: Add items */}
      <div style={{ marginTop: '20px' }}>
        <button onClick={() => dispatch(actions.addToCart(1))}>
          Add iPhone
        </button>
        <button onClick={() => dispatch(actions.addToCart(2))}>
          Add MacBook
        </button>
        <button onClick={() => dispatch(actions.addToCart(3))}>
          Add AirPods
        </button>
        <button onClick={() => dispatch(actions.addToCart(4))}>
          Add React Book
        </button>
      </div>
    </div>
  );
}

/*
Kết quả ví dụ:

1. Nhấn "Add iPhone" → Cart: iPhone (1) = $999
2. Nhấn "Add iPhone" lần nữa → Cart: iPhone (2) = $1998
3. Nhấn "Add React Book" → Cart: iPhone (2) = $1998, React Book (1) = $49
4. Nhấn "-" trên iPhone → Cart: iPhone (1) = $999, React Book (1) = $49
5. Nhấn "Remove" trên React Book → Cart: iPhone (1) = $999
6. Nhấn "-" trên iPhone → Cart: empty
*/

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

jsx
/**
 * 🎯 Mục tiêu: Build Normalized State cho Messaging App
 * ⏱️ Thời gian: 40 phút
 *
 * 📋 Product Requirements:
 * User Story: "Là user, tôi muốn chat với friends trong app"
 *
 * ✅ Acceptance Criteria:
 * - [ ] Hiển thị list conversations
 * - [ ] Click conversation → hiển thị messages
 * - [ ] Send message trong conversation
 * - [ ] Delete message
 * - [ ] Mark conversation as read/unread
 *
 * 🎨 State Structure (Normalized):
 * {
 *   users: { [id]: { id, name, avatar } },
 *   conversations: { [id]: { id, participantIds: [], lastMessageId } },
 *   messages: { [id]: { id, conversationId, senderId, text, timestamp } },
 *   conversationMessageIds: { [conversationId]: [messageId, ...] }
 * }
 *
 * 🚨 Edge Cases:
 * - Delete message → update lastMessageId nếu là message cuối
 * - Delete conversation → cascade delete messages
 * - Send message → update lastMessageId trong conversation
 *
 * 📝 Implementation Checklist:
 * - [ ] ActionTypes constants (8+ actions)
 * - [ ] Action creators
 * - [ ] Reducer với normalized updates
 * - [ ] Helper functions (getConversationWithMessages, etc.)
 * - [ ] Component display conversations + messages
 */

// TODO: Full implementation

// Gợi ý Actions:
// - ADD_CONVERSATION
// - DELETE_CONVERSATION
// - SEND_MESSAGE
// - DELETE_MESSAGE
// - MARK_READ
// - MARK_UNREAD

// Gợi ý Initial State:
const initialState = {
  users: {
    1: { id: 1, name: 'Alice', avatar: '👩' },
    2: { id: 2, name: 'Bob', avatar: '👨' },
  },
  conversations: {},
  messages: {},
  conversationMessageIds: {},
  allConversationIds: [],
};
💡 Solution
jsx
/**
 * Messaging App - Normalized State with Conversations & Messages
 *
 * Features:
 * - List conversations
 * - View messages in selected conversation
 * - Send new message
 * - Delete message (with lastMessageId update)
 * - Mark conversation as read/unread
 *
 * @returns {JSX.Element}
 */
function MessagingApp() {
  // ── Action Type Constants ────────────────────────────────────────
  const ActionTypes = {
    ADD_CONVERSATION: 'ADD_CONVERSATION',
    DELETE_CONVERSATION: 'DELETE_CONVERSATION',
    SEND_MESSAGE: 'SEND_MESSAGE',
    DELETE_MESSAGE: 'DELETE_MESSAGE',
    MARK_READ: 'MARK_READ',
    MARK_UNREAD: 'MARK_UNREAD',
    SELECT_CONVERSATION: 'SELECT_CONVERSATION',
  };

  // ── Action Creators ──────────────────────────────────────────────
  const actions = {
    addConversation: (participantIds, title = '') => ({
      type: ActionTypes.ADD_CONVERSATION,
      payload: {
        id: Date.now().toString(),
        participantIds,
        title,
        createdAt: new Date().toISOString(),
      },
    }),

    deleteConversation: (conversationId) => ({
      type: ActionTypes.DELETE_CONVERSATION,
      payload: { conversationId },
    }),

    sendMessage: (conversationId, senderId, text) => ({
      type: ActionTypes.SEND_MESSAGE,
      payload: {
        id: Date.now().toString(),
        conversationId,
        senderId,
        text,
        timestamp: new Date().toISOString(),
      },
    }),

    deleteMessage: (conversationId, messageId) => ({
      type: ActionTypes.DELETE_MESSAGE,
      payload: { conversationId, messageId },
    }),

    markRead: (conversationId) => ({
      type: ActionTypes.MARK_READ,
      payload: { conversationId },
    }),

    markUnread: (conversationId) => ({
      type: ActionTypes.MARK_UNREAD,
      payload: { conversationId },
    }),

    selectConversation: (conversationId) => ({
      type: ActionTypes.SELECT_CONVERSATION,
      payload: { conversationId },
    }),
  };

  // ── Initial State ────────────────────────────────────────────────
  const initialState = {
    users: {
      1: { id: '1', name: 'Alice', avatar: '👩' },
      2: { id: '2', name: 'Bob', avatar: '👨' },
      3: { id: '3', name: 'Charlie', avatar: '🧑' },
    },
    conversations: {}, // { convId: { id, participantIds, lastMessageId?, unread: boolean, title?, createdAt } }
    messages: {}, // { msgId: { id, conversationId, senderId, text, timestamp } }
    conversationMessageIds: {}, // { convId: [msgId, msgId, ...] } – ordered
    selectedConversationId: null,
    allConversationIds: [],
  };

  // ── Reducer ──────────────────────────────────────────────────────
  const reducer = (state, action) => {
    switch (action.type) {
      case ActionTypes.ADD_CONVERSATION: {
        const { id, participantIds, title, createdAt } = action.payload;
        return {
          ...state,
          conversations: {
            ...state.conversations,
            [id]: { id, participantIds, unread: false, title, createdAt },
          },
          conversationMessageIds: {
            ...state.conversationMessageIds,
            [id]: [],
          },
          allConversationIds: [...state.allConversationIds, id],
        };
      }

      case ActionTypes.DELETE_CONVERSATION: {
        const { conversationId } = action.payload;
        const { [conversationId]: removedConv, ...restConvs } =
          state.conversations;
        const { [conversationId]: removedIds, ...restMsgIds } =
          state.conversationMessageIds;

        // Xóa tất cả messages liên quan
        const msgIdsToDelete = removedIds || [];
        const newMessages = { ...state.messages };
        msgIdsToDelete.forEach((msgId) => delete newMessages[msgId]);

        return {
          ...state,
          conversations: restConvs,
          messages: newMessages,
          conversationMessageIds: restMsgIds,
          allConversationIds: state.allConversationIds.filter(
            (id) => id !== conversationId,
          ),
          selectedConversationId:
            state.selectedConversationId === conversationId
              ? null
              : state.selectedConversationId,
        };
      }

      case ActionTypes.SEND_MESSAGE: {
        const { id, conversationId, senderId, text, timestamp } =
          action.payload;

        return {
          ...state,
          messages: {
            ...state.messages,
            [id]: { id, conversationId, senderId, text, timestamp },
          },
          conversationMessageIds: {
            ...state.conversationMessageIds,
            [conversationId]: [
              ...(state.conversationMessageIds[conversationId] || []),
              id,
            ],
          },
          conversations: {
            ...state.conversations,
            [conversationId]: {
              ...state.conversations[conversationId],
              lastMessageId: id,
              unread: senderId !== '1', // giả sử current user là id '1'
            },
          },
        };
      }

      case ActionTypes.DELETE_MESSAGE: {
        const { conversationId, messageId } = action.payload;

        const { [messageId]: removedMsg, ...restMessages } = state.messages;

        const currentMsgIds =
          state.conversationMessageIds[conversationId] || [];
        const newMsgIds = currentMsgIds.filter((id) => id !== messageId);

        let newLastMessageId =
          state.conversations[conversationId]?.lastMessageId;
        if (newLastMessageId === messageId) {
          newLastMessageId =
            newMsgIds.length > 0 ? newMsgIds[newMsgIds.length - 1] : null;
        }

        return {
          ...state,
          messages: restMessages,
          conversationMessageIds: {
            ...state.conversationMessageIds,
            [conversationId]: newMsgIds,
          },
          conversations: {
            ...state.conversations,
            [conversationId]: {
              ...state.conversations[conversationId],
              lastMessageId: newLastMessageId,
            },
          },
        };
      }

      case ActionTypes.MARK_READ:
      case ActionTypes.MARK_UNREAD: {
        const { conversationId } = action.payload;
        return {
          ...state,
          conversations: {
            ...state.conversations,
            [conversationId]: {
              ...state.conversations[conversationId],
              unread: action.type === ActionTypes.MARK_UNREAD,
            },
          },
        };
      }

      case ActionTypes.SELECT_CONVERSATION: {
        const { conversationId } = action.payload;
        return {
          ...state,
          selectedConversationId: conversationId,
          // Tự động mark read khi mở conversation (tùy chọn)
          conversations: {
            ...state.conversations,
            [conversationId]: {
              ...state.conversations[conversationId],
              unread: false,
            },
          },
        };
      }

      default:
        return state;
    }
  };

  const [state, dispatch] = React.useReducer(reducer, initialState);

  // ── Helpers ──────────────────────────────────────────────────────
  const getConversationWithParticipants = (convId) => {
    const conv = state.conversations[convId];
    if (!conv) return null;
    return {
      ...conv,
      participants: conv.participantIds.map((id) => state.users[id]),
    };
  };

  const getMessagesForConversation = (convId) => {
    const msgIds = state.conversationMessageIds[convId] || [];
    return msgIds.map((id) => state.messages[id]);
  };

  const currentConv = state.selectedConversationId
    ? getConversationWithParticipants(state.selectedConversationId)
    : null;

  const currentMessages = state.selectedConversationId
    ? getMessagesForConversation(state.selectedConversationId)
    : [];

  // ── UI ───────────────────────────────────────────────────────────
  return (
    <div style={{ display: 'flex', height: '80vh', fontFamily: 'system-ui' }}>
      {/* Sidebar - Conversations */}
      <div
        style={{
          width: '280px',
          borderRight: '1px solid #ddd',
          padding: '16px',
        }}
      >
        <h3>Chats</h3>
        <button
          onClick={() =>
            dispatch(actions.addConversation(['1', '2'], 'Alice & You'))
          }
          style={{ marginBottom: '12px' }}
        >
          New Chat with Alice
        </button>
        <button
          onClick={() =>
            dispatch(actions.addConversation(['1', '3'], 'Charlie Group'))
          }
        >
          New Chat with Charlie
        </button>

        <ul style={{ listStyle: 'none', padding: 0, marginTop: '16px' }}>
          {state.allConversationIds.map((convId) => {
            const conv = getConversationWithParticipants(convId);
            if (!conv) return null;
            const lastMsgId = conv.lastMessageId;
            const lastMsg = lastMsgId ? state.messages[lastMsgId] : null;
            const lastSender = lastMsg
              ? state.users[lastMsg.senderId]?.name
              : '';

            return (
              <li
                key={convId}
                onClick={() => dispatch(actions.selectConversation(convId))}
                style={{
                  padding: '12px',
                  marginBottom: '8px',
                  background:
                    state.selectedConversationId === convId
                      ? '#e0f2fe'
                      : '#f8f9fa',
                  borderRadius: '8px',
                  cursor: 'pointer',
                  border: conv.unread
                    ? '1px solid #3b82f6'
                    : '1px solid transparent',
                }}
              >
                <strong>
                  {conv.title ||
                    conv.participants.map((p) => p.name).join(', ')}
                </strong>
                {lastMsg && (
                  <div
                    style={{
                      fontSize: '0.9em',
                      color: '#666',
                      marginTop: '4px',
                    }}
                  >
                    {lastSender}: {lastMsg.text.slice(0, 40)}
                    {lastMsg.text.length > 40 ? '...' : ''}
                  </div>
                )}
                {conv.unread && (
                  <span style={{ color: '#3b82f6', fontWeight: 'bold' }}>
                    {' '}

                  </span>
                )}
              </li>
            );
          })}
        </ul>
      </div>

      {/* Main Chat Area */}
      <div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
        {currentConv ? (
          <>
            <div
              style={{
                padding: '16px',
                borderBottom: '1px solid #ddd',
                background: '#f8f9fa',
              }}
            >
              <h2>
                {currentConv.title ||
                  currentConv.participants.map((p) => p.name).join(', ')}
              </h2>
              {currentConv.unread ? (
                <button
                  onClick={() => dispatch(actions.markRead(currentConv.id))}
                >
                  Mark as Read
                </button>
              ) : (
                <button
                  onClick={() => dispatch(actions.markUnread(currentConv.id))}
                >
                  Mark as Unread
                </button>
              )}
              <button
                onClick={() =>
                  dispatch(actions.deleteConversation(currentConv.id))
                }
                style={{ marginLeft: '12px', color: 'red' }}
              >
                Delete Conversation
              </button>
            </div>

            <div style={{ flex: 1, overflowY: 'auto', padding: '16px' }}>
              {currentMessages.map((msg) => {
                const isMe = msg.senderId === '1';
                const sender = state.users[msg.senderId];

                return (
                  <div
                    key={msg.id}
                    style={{
                      marginBottom: '16px',
                      textAlign: isMe ? 'right' : 'left',
                      display: 'flex',
                      flexDirection: 'column',
                      alignItems: isMe ? 'flex-end' : 'flex-start',
                    }}
                  >
                    <small style={{ color: '#666', marginBottom: '4px' }}>
                      {sender.name} •{' '}
                      {new Date(msg.timestamp).toLocaleTimeString([], {
                        hour: '2-digit',
                        minute: '2-digit',
                      })}
                    </small>
                    <div
                      style={{
                        maxWidth: '70%',
                        padding: '10px 14px',
                        borderRadius: '18px',
                        background: isMe ? '#3b82f6' : '#e5e7eb',
                        color: isMe ? 'white' : 'black',
                        position: 'relative',
                      }}
                    >
                      {msg.text}
                      <button
                        onClick={() =>
                          dispatch(
                            actions.deleteMessage(currentConv.id, msg.id),
                          )
                        }
                        style={{
                          fontSize: '0.7em',
                          marginLeft: '8px',
                          background: 'none',
                          border: 'none',
                          color: isMe ? '#bfdbfe' : '#9ca3af',
                          cursor: 'pointer',
                        }}
                      >
                        ×
                      </button>
                    </div>
                  </div>
                );
              })}
            </div>

            <div
              style={{
                padding: '16px',
                borderTop: '1px solid #ddd',
                display: 'flex',
              }}
            >
              <input
                type='text'
                placeholder='Type a message...'
                id='messageInput'
                style={{
                  flex: 1,
                  padding: '10px',
                  borderRadius: '20px',
                  border: '1px solid #ddd',
                }}
              />
              <button
                onClick={() => {
                  const input = document.getElementById('messageInput');
                  if (input.value.trim()) {
                    dispatch(
                      actions.sendMessage(
                        currentConv.id,
                        '1', // current user
                        input.value.trim(),
                      ),
                    );
                    input.value = '';
                  }
                }}
                style={{
                  marginLeft: '12px',
                  padding: '10px 20px',
                  borderRadius: '20px',
                  background: '#3b82f6',
                  color: 'white',
                  border: 'none',
                }}
              >
                Send
              </button>
            </div>
          </>
        ) : (
          <div
            style={{
              flex: 1,
              display: 'flex',
              alignItems: 'center',
              justifyContent: 'center',
              color: '#888',
            }}
          >
            Select a conversation to start chatting
          </div>
        )}
      </div>
    </div>
  );
}

/*
Kết quả ví dụ:

1. Nhấn "New Chat with Alice" → xuất hiện conversation mới
2. Chọn conversation → thấy giao diện chat trống
3. Gõ "Hey Alice!" → nhấn Send → tin nhắn hiện bên phải (màu xanh)
4. Gõ "How are you?" → tin nhắn thứ hai
5. Nhấn × bên cạnh tin nhắn đầu → tin nhắn bị xóa, lastMessageId cập nhật
6. Nhấn "Mark as Unread" → conversation có dấu chấm xanh
7. Nhấn "Delete Conversation" → conversation và tất cả messages biến mất
*/

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

jsx
/**
 * 🎯 Mục tiêu: Design State Architecture cho Large App
 * ⏱️ Thời gian: 60 phút
 *
 * 🏗️ PHASE 1: Research & Design (20 phút)
 *
 * Context: Spotify-like Music Player App
 *
 * Features:
 * - Browse playlists (user's + public)
 * - Each playlist có songs
 * - Play song → update currently playing
 * - Queue management (add to queue, reorder)
 * - Playback state (playing, paused, volume, progress)
 * - User can create/edit/delete playlists
 * - User can favorite songs
 *
 * State Architecture Options:
 *
 * Option A: Single Normalized State
 * {
 *   playlists: { [id]: {...} },
 *   songs: { [id]: {...} },
 *   playlistSongIds: { [playlistId]: [songId, ...] },
 *   player: { currentSongId, isPlaying, volume, ... },
 *   queue: [songId, ...],
 *   favorites: [songId, ...]
 * }
 *
 * Option B: Domain-Separated State
 * {
 *   library: {
 *     playlists: {...},
 *     songs: {...},
 *     playlistSongIds: {...}
 *   },
 *   player: { currentSongId, isPlaying, ... },
 *   queue: [...],
 *   user: { favorites: [...] }
 * }
 *
 * Option C: Reducer Composition
 * - libraryReducer (playlists, songs)
 * - playerReducer (playback state)
 * - queueReducer (queue management)
 * - userReducer (favorites, settings)
 * - Combine với rootReducer
 *
 * Nhiệm vụ:
 * 1. So sánh 3 options
 * 2. Consider:
 *    - Update frequency (player state updates often)
 *    - Cross-domain logic (play song from playlist → update player + queue)
 *    - Testability
 *    - Code organization
 * 3. Viết ADR
 *
 * 📝 ADR Template:
 *
 * ## Context
 * Music player cần quản lý library (playlists, songs), player state,
 * queue, và user preferences. Player state updates rất thường xuyên
 * (mỗi giây), trong khi library data ít thay đổi.
 *
 * ## Decision
 * Chọn Option C: Reducer Composition
 *
 * ## Rationale
 * - Player updates không trigger re-render library data
 * - Each domain isolated, easy to test
 * - Clear code organization (player logic ở playerReducer)
 * - Scalable (dễ add features)
 *
 * ## Consequences
 * Trade-offs:
 * + Better performance (isolated updates)
 * + Maintainable (clear boundaries)
 * + Testable (test each reducer)
 * - More boilerplate (4 reducers + root)
 * - Cross-domain actions cần handle carefully
 *
 * ## Alternatives Considered
 * - Option A: Simple nhưng player updates slow
 * - Option B: Better nhưng không có clear boundaries
 *
 * 💻 PHASE 2: Implementation (30 phút)
 * Implement architecture đã chọn:
 * - Define state shape
 * - Create sub-reducers
 * - Create rootReducer
 * - Action types + creators (at least 10)
 * - 2-3 actions demonstrate cross-domain logic
 *
 * Example cross-domain action:
 * PLAY_SONG:
 * - playerReducer: Update currentSongId, isPlaying
 * - queueReducer: Add to queue nếu chưa có
 *
 * 🧪 PHASE 3: Testing (10 phút)
 * Write pseudo-code tests:
 * - Test playerReducer isolated
 * - Test PLAY_SONG updates both player & queue
 * - Test DELETE_PLAYLIST cascade deletes
 */

// TODO: Viết ADR + Implementation + Tests
💡 Solution
jsx
/**
 * Level 4: Quyết Định Kiến Trúc - Spotify-like Music Player App
 *
 * Architecture Decision Record (ADR)
 *
 * ## Context
 * Ứng dụng music player cần quản lý:
 * - Thư viện (playlists, songs)
 * - Trạng thái phát nhạc (current song, playing/paused, volume, progress)
 * - Queue (danh sách phát tiếp theo, có thể reorder)
 * - User preferences (favorites, settings)
 *
 * Đặc điểm quan trọng:
 * - Player state cập nhật rất thường xuyên (progress mỗi giây, play/pause, volume...)
 * - Library data thay đổi ít hơn (thêm playlist, favorite song...)
 * - Có cross-domain actions: PLAY_SONG → update player + queue
 * - Cần testability cao và tổ chức code rõ ràng khi scale
 *
 * ## Decision
 * Chọn **Option C: Reducer Composition** với 4 sub-reducers riêng biệt
 *
 * State shape:
 * {
 *   library: { playlists, songs, playlistSongIds },
 *   player: { currentSongId, isPlaying, volume, progress, ... },
 *   queue: { songIds: [], currentIndex },
 *   user: { favorites: [songId,...], settings }
 * }
 *
 * ## Rationale
 * - Player updates (progress, volume) không làm re-render toàn bộ library → performance tốt
 * - Mỗi domain có reducer riêng → code ngắn gọn, dễ maintain, dễ test
 * - Cross-domain actions dễ xử lý bằng cách để rootReducer gọi nhiều sub-reducer
 * - Queue và player tách biệt → dễ implement shuffle, repeat, reorder
 * - Scalable: dễ thêm domain mới (ví dụ: recommendations, offline mode)
 *
 * ## Consequences
 * + Performance tốt hơn (isolated updates)
 * + Code organization rõ ràng (player logic chỉ nằm trong playerReducer)
 * + Test mỗi reducer độc lập
 * - Boilerplate nhiều hơn (4 reducers + rootReducer)
 * - Cross-domain actions cần cẩn thận (phải pass action cho nhiều reducer)
 *
 * ## Alternatives Considered
 * - Option A (Single Normalized State): đơn giản nhưng player updates gây re-render không cần thiết
 * - Option B (Domain-Separated State): tốt hơn A nhưng không có boundary rõ ràng giữa các domain
 *
 * @returns {JSX.Element}
 */
function MusicPlayerApp() {
  // ── Action Type Constants ────────────────────────────────────────
  const ActionTypes = {
    // Library
    ADD_PLAYLIST: 'ADD_PLAYLIST',
    ADD_SONG_TO_PLAYLIST: 'ADD_SONG_TO_PLAYLIST',
    REMOVE_SONG_FROM_PLAYLIST: 'REMOVE_SONG_FROM_PLAYLIST',
    TOGGLE_FAVORITE: 'TOGGLE_FAVORITE',

    // Player
    PLAY_SONG: 'PLAY_SONG',
    PAUSE: 'PAUSE',
    PLAY: 'PLAY',
    NEXT: 'NEXT',
    PREVIOUS: 'PREVIOUS',
    SET_VOLUME: 'SET_VOLUME',
    SET_PROGRESS: 'SET_PROGRESS',

    // Queue
    ADD_TO_QUEUE: 'ADD_TO_QUEUE',
    CLEAR_QUEUE: 'CLEAR_QUEUE',
    REORDER_QUEUE: 'REORDER_QUEUE',
  };

  // ── Action Creators ──────────────────────────────────────────────
  const actions = {
    // Library
    addPlaylist: (title) => ({
      type: ActionTypes.ADD_PLAYLIST,
      payload: { id: Date.now().toString(), title },
    }),
    addSongToPlaylist: (playlistId, songId) => ({
      type: ActionTypes.ADD_SONG_TO_PLAYLIST,
      payload: { playlistId, songId },
    }),
    toggleFavorite: (songId) => ({
      type: ActionTypes.TOGGLE_FAVORITE,
      payload: { songId },
    }),

    // Player + Queue cross-domain
    playSong: (songId, fromQueue = false) => ({
      type: ActionTypes.PLAY_SONG,
      payload: { songId, fromQueue },
    }),
    play: () => ({ type: ActionTypes.PLAY }),
    pause: () => ({ type: ActionTypes.PAUSE }),
    next: () => ({ type: ActionTypes.NEXT }),
    previous: () => ({ type: ActionTypes.PREVIOUS }),
    setVolume: (volume) => ({
      type: ActionTypes.SET_VOLUME,
      payload: { volume },
    }),
    setProgress: (progress) => ({
      type: ActionTypes.SET_PROGRESS,
      payload: { progress },
    }),

    // Queue
    addToQueue: (songId) => ({
      type: ActionTypes.ADD_TO_QUEUE,
      payload: { songId },
    }),
  };

  // ── Initial State ────────────────────────────────────────────────
  const initialState = {
    library: {
      songs: {
        s1: {
          id: 's1',
          title: 'Blinding Lights',
          artist: 'The Weeknd',
          duration: 200,
        },
        s2: {
          id: 's2',
          title: 'Levitating',
          artist: 'Dua Lipa',
          duration: 203,
        },
        s3: {
          id: 's3',
          title: 'Save Your Tears',
          artist: 'The Weeknd',
          duration: 215,
        },
      },
      playlists: {},
      playlistSongIds: {},
    },
    player: {
      currentSongId: null,
      isPlaying: false,
      volume: 80,
      progress: 0, // 0-100
    },
    queue: {
      songIds: [],
      currentIndex: -1,
    },
    user: {
      favorites: [],
    },
  };

  // ── Sub-reducers ─────────────────────────────────────────────────

  function libraryReducer(state = initialState.library, action) {
    switch (action.type) {
      case ActionTypes.ADD_PLAYLIST: {
        const { id, title } = action.payload;
        return {
          ...state,
          playlists: {
            ...state.playlists,
            [id]: { id, title, createdAt: new Date().toISOString() },
          },
          playlistSongIds: {
            ...state.playlistSongIds,
            [id]: [],
          },
        };
      }

      case ActionTypes.ADD_SONG_TO_PLAYLIST: {
        const { playlistId, songId } = action.payload;
        return {
          ...state,
          playlistSongIds: {
            ...state.playlistSongIds,
            [playlistId]: [
              ...(state.playlistSongIds[playlistId] || []),
              songId,
            ],
          },
        };
      }

      default:
        return state;
    }
  }

  function playerReducer(state = initialState.player, action, queueState) {
    switch (action.type) {
      case ActionTypes.PLAY_SONG: {
        const { songId, fromQueue } = action.payload;
        return {
          ...state,
          currentSongId: songId,
          isPlaying: true,
          progress: 0,
        };
      }

      case ActionTypes.PLAY:
        return { ...state, isPlaying: true };

      case ActionTypes.PAUSE:
        return { ...state, isPlaying: false };

      case ActionTypes.SET_VOLUME:
        return {
          ...state,
          volume: Math.max(0, Math.min(100, action.payload.volume)),
        };

      case ActionTypes.SET_PROGRESS:
        return { ...state, progress: action.payload.progress };

      // NEXT / PREVIOUS sẽ được xử lý ở rootReducer
      default:
        return state;
    }
  }

  function queueReducer(state = initialState.queue, action) {
    switch (action.type) {
      case ActionTypes.PLAY_SONG: {
        const { songId, fromQueue } = action.payload;
        if (fromQueue) {
          const index = state.songIds.indexOf(songId);
          return index !== -1 ? { ...state, currentIndex: index } : state;
        }
        // Add to queue if not already playing from queue
        if (!state.songIds.includes(songId)) {
          return {
            ...state,
            songIds: [...state.songIds, songId],
            currentIndex: state.songIds.length,
          };
        }
        return { ...state, currentIndex: state.songIds.indexOf(songId) };
      }

      case ActionTypes.ADD_TO_QUEUE: {
        const { songId } = action.payload;
        if (state.songIds.includes(songId)) return state;
        return { ...state, songIds: [...state.songIds, songId] };
      }

      case ActionTypes.NEXT: {
        const nextIndex = state.currentIndex + 1;
        if (nextIndex >= state.songIds.length) return state;
        return { ...state, currentIndex: nextIndex };
      }

      case ActionTypes.PREVIOUS: {
        const prevIndex = state.currentIndex - 1;
        if (prevIndex < 0) return state;
        return { ...state, currentIndex: prevIndex };
      }

      default:
        return state;
    }
  }

  function userReducer(state = initialState.user, action) {
    switch (action.type) {
      case ActionTypes.TOGGLE_FAVORITE: {
        const { songId } = action.payload;
        const isFavorite = state.favorites.includes(songId);
        return {
          ...state,
          favorites: isFavorite
            ? state.favorites.filter((id) => id !== songId)
            : [...state.favorites, songId],
        };
      }

      default:
        return state;
    }
  }

  // ── Root Reducer (Composition) ───────────────────────────────────
  function rootReducer(state, action) {
    const library = libraryReducer(state.library, action);
    const queue = queueReducer(state.queue, action);
    const player = playerReducer(state.player, action, queue);
    const user = userReducer(state.user, action);

    // Cross-domain: khi NEXT/PREVIOUS → update player.currentSongId
    let finalPlayer = player;
    if (
      action.type === ActionTypes.NEXT ||
      action.type === ActionTypes.PREVIOUS
    ) {
      const newSongId = queue.songIds[queue.currentIndex] || null;
      finalPlayer = {
        ...player,
        currentSongId: newSongId,
        isPlaying: newSongId ? player.isPlaying : false,
      };
    }

    // PLAY_SONG từ playlist → tự động add vào queue nếu cần
    if (action.type === ActionTypes.PLAY_SONG && !action.payload.fromQueue) {
      // Logic add to queue đã xử lý trong queueReducer
    }

    return {
      ...state,
      library,
      player: finalPlayer,
      queue,
      user,
    };
  }

  const [state, dispatch] = React.useReducer(rootReducer, initialState);

  // ── Helpers ──────────────────────────────────────────────────────
  const currentSong = state.player.currentSongId
    ? state.library.songs[state.player.currentSongId]
    : null;

  // ── UI (Simplified) ──────────────────────────────────────────────
  return (
    <div style={{ padding: '20px', fontFamily: 'system-ui' }}>
      <h1>Music Player</h1>

      {/* Now Playing */}
      <div
        style={{
          marginBottom: '24px',
          padding: '16px',
          background: '#f8f9fa',
          borderRadius: '12px',
        }}
      >
        <h3>Now Playing</h3>
        {currentSong ? (
          <div>
            <strong>{currentSong.title}</strong> — {currentSong.artist}
            <div style={{ marginTop: '12px' }}>
              <button onClick={() => dispatch(actions.play())}>Play</button>
              <button onClick={() => dispatch(actions.pause())}>Pause</button>
              <button onClick={() => dispatch(actions.previous())}>
                ◀ Previous
              </button>
              <button onClick={() => dispatch(actions.next())}>Next ▶</button>
            </div>
            <div>
              Volume:
              <input
                type='range'
                min='0'
                max='100'
                value={state.player.volume}
                onChange={(e) =>
                  dispatch(actions.setVolume(Number(e.target.value)))
                }
              />
            </div>
            <div>Progress: {state.player.progress}%</div>
          </div>
        ) : (
          <p>Select a song to play</p>
        )}
      </div>

      {/* Controls */}
      <div style={{ marginBottom: '24px' }}>
        <button onClick={() => dispatch(actions.addPlaylist('Chill Vibes'))}>
          Create Playlist
        </button>
        <button onClick={() => dispatch(actions.playSong('s1'))}>
          Play Blinding Lights
        </button>
        <button onClick={() => dispatch(actions.playSong('s2'))}>
          Play Levitating
        </button>
        <button onClick={() => dispatch(actions.addToQueue('s3'))}>
          Add Save Your Tears to Queue
        </button>
        <button onClick={() => dispatch(actions.toggleFavorite('s1'))}>
          {state.user.favorites.includes('s1') ? 'Unfavorite' : 'Favorite'}{' '}
          Blinding Lights
        </button>
      </div>

      {/* Queue */}
      <div>
        <h3>Queue ({state.queue.songIds.length})</h3>
        <ul>
          {state.queue.songIds.map((songId, index) => {
            const song = state.library.songs[songId];
            return (
              <li
                key={songId}
                style={{
                  fontWeight:
                    index === state.queue.currentIndex ? 'bold' : 'normal',
                  color:
                    index === state.queue.currentIndex ? '#3b82f6' : 'inherit',
                }}
              >
                {song.title} — {song.artist}
              </li>
            );
          })}
        </ul>
      </div>
    </div>
  );
}

/*
Kết quả ví dụ:

1. Nhấn "Play Blinding Lights" → player chơi bài s1, queue = ['s1'], currentIndex = 0
2. Nhấn "Add Save Your Tears to Queue" → queue = ['s1', 's3']
3. Nhấn "Next" → chuyển sang s3, currentSongId = 's3'
4. Nhấn "Favorite Blinding Lights" → favorites = ['s1']
5. Nhấn "Create Playlist" → playlists có thêm một playlist mới
6. Nhấn "Play" / "Pause" / thay đổi volume → chỉ player state thay đổi, không re-render queue/library
*/

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

jsx
/**
 * 🎯 Mục tiêu: Build Production-Ready Kanban Board
 * ⏱️ Thời gian: 90 phút
 *
 * 📋 Feature Specification:
 *
 * Trello-like Kanban Board với:
 * - Multiple boards
 * - Each board có columns (Todo, In Progress, Done)
 * - Each column có cards
 * - Drag & drop cards giữa columns
 * - Add/Edit/Delete cards
 * - Add/Edit/Delete columns
 * - Card tags/labels
 * - Filter cards by tag
 * - Search cards
 * - Card details (title, description, due date, assignee)
 *
 * 🏗️ Technical Design Doc:
 *
 * 1. State Architecture:
 *    Normalized state với:
 *    - boards: { [id]: { id, title, columnIds } }
 *    - columns: { [id]: { id, title, boardId, cardIds } }
 *    - cards: { [id]: { id, title, description, columnId, tags, ... } }
 *    - tags: { [id]: { id, name, color } }
 *
 * 2. Reducer Composition:
 *    - boardsReducer
 *    - columnsReducer
 *    - cardsReducer
 *    - tagsReducer
 *    - filterReducer
 *    - rootReducer (combine all)
 *
 * 3. Action Types (20+ actions):
 *    Boards: ADD_BOARD, EDIT_BOARD, DELETE_BOARD
 *    Columns: ADD_COLUMN, EDIT_COLUMN, DELETE_COLUMN, REORDER_COLUMNS
 *    Cards: ADD_CARD, EDIT_CARD, DELETE_CARD, MOVE_CARD
 *    Tags: ADD_TAG, EDIT_TAG, DELETE_TAG
 *    Filters: SET_TAG_FILTER, SET_SEARCH_FILTER, CLEAR_FILTERS
 *
 * 4. Cross-Domain Logic:
 *    - DELETE_BOARD: cascade delete columns & cards
 *    - DELETE_COLUMN: move cards to "Unassigned" hoặc delete
 *    - MOVE_CARD: update cardIds trong 2 columns
 *    - DELETE_TAG: remove tag từ tất cả cards
 *
 * 5. Performance:
 *    - useMemo for filtered/searched cards
 *    - Denormalize helpers cached
 *
 * ✅ Production Checklist:
 * - [ ] State shape documented (comments)
 * - [ ] All ActionTypes constants defined
 * - [ ] All action creators implemented
 * - [ ] All reducers implemented (4-5 reducers)
 * - [ ] rootReducer combines all
 * - [ ] Cross-domain logic handled correctly
 * - [ ] Helper functions:
 *   - [ ] getBoardWithColumns
 *   - [ ] getColumnWithCards
 *   - [ ] getFilteredCards
 * - [ ] Edge cases handled:
 *   - [ ] Delete board with columns/cards
 *   - [ ] Move card updates both columns
 *   - [ ] Filter by multiple tags
 * - [ ] Code quality:
 *   - [ ] Clear comments
 *   - [ ] Consistent naming
 *   - [ ] DRY (no duplication)
 *
 * 📝 Documentation:
 * - README với state architecture diagram
 * - Action types reference
 * - Reducer responsibilities
 *
 * 🔍 Self-Review Checklist:
 * - [ ] Immutable updates everywhere
 * - [ ] Default cases in all reducers
 * - [ ] Action creators type-safe (payload structure consistent)
 * - [ ] No magic strings (all constants)
 * - [ ] Normalized state (no nested arrays/objects)
 */

// TODO: Full implementation

// Starter: State Shape
const initialState = {
  boards: {
    // '1': { id: '1', title: 'My Board', columnIds: ['col1', 'col2'] }
  },
  columns: {
    // 'col1': { id: 'col1', title: 'Todo', boardId: '1', cardIds: ['card1'] }
  },
  cards: {
    // 'card1': { id: 'card1', title: 'Task', description: '...', columnId: 'col1', tagIds: ['tag1'] }
  },
  tags: {
    // 'tag1': { id: 'tag1', name: 'Bug', color: 'red' }
  },
  filters: {
    tagIds: [],
    searchText: '',
  },
  allBoardIds: [],
  allTagIds: [],
};

// Starter: ActionTypes
const ActionTypes = {
  // Boards
  ADD_BOARD: 'ADD_BOARD',
  EDIT_BOARD: 'EDIT_BOARD',
  DELETE_BOARD: 'DELETE_BOARD',

  // Columns
  ADD_COLUMN: 'ADD_COLUMN',
  EDIT_COLUMN: 'EDIT_COLUMN',
  DELETE_COLUMN: 'DELETE_COLUMN',
  REORDER_COLUMNS: 'REORDER_COLUMNS',

  // Cards
  ADD_CARD: 'ADD_CARD',
  EDIT_CARD: 'EDIT_CARD',
  DELETE_CARD: 'DELETE_CARD',
  MOVE_CARD: 'MOVE_CARD',

  // Tags
  ADD_TAG: 'ADD_TAG',
  EDIT_TAG: 'EDIT_TAG',
  DELETE_TAG: 'DELETE_TAG',

  // Filters
  SET_TAG_FILTER: 'SET_TAG_FILTER',
  SET_SEARCH_FILTER: 'SET_SEARCH_FILTER',
  CLEAR_FILTERS: 'CLEAR_FILTERS',
};

// TODO: Implement action creators
// TODO: Implement reducers
// TODO: Implement component
💡 Solution
jsx
/**
 * Production-Ready Kanban Board (Trello-like)
 * Level 5 Challenge - Normalized State + Reducer Composition
 *
 * Features implemented:
 * - Multiple boards
 * - Columns per board
 * - Cards with title, description, dueDate, assignee, tags
 * - Drag & drop simulation (click to move)
 * - Add/Edit/Delete board/column/card
 * - Tag management
 * - Basic tag filter + search
 *
 * @returns {JSX.Element}
 */
function KanbanBoardApp() {
  // ── ACTION TYPES ─────────────────────────────────────────────────
  const ActionTypes = {
    // Boards
    ADD_BOARD: 'ADD_BOARD',
    EDIT_BOARD: 'EDIT_BOARD',
    DELETE_BOARD: 'DELETE_BOARD',

    // Columns
    ADD_COLUMN: 'ADD_COLUMN',
    EDIT_COLUMN: 'EDIT_COLUMN',
    DELETE_COLUMN: 'DELETE_COLUMN',
    REORDER_COLUMNS: 'REORDER_COLUMNS',

    // Cards
    ADD_CARD: 'ADD_CARD',
    EDIT_CARD: 'EDIT_CARD',
    DELETE_CARD: 'DELETE_CARD',
    MOVE_CARD: 'MOVE_CARD',

    // Tags
    ADD_TAG: 'ADD_TAG',
    EDIT_TAG: 'EDIT_TAG',
    DELETE_TAG: 'DELETE_TAG',

    // Filters
    SET_TAG_FILTER: 'SET_TAG_FILTER',
    SET_SEARCH_FILTER: 'SET_SEARCH_FILTER',
    CLEAR_FILTERS: 'CLEAR_FILTERS',

    // UI
    SELECT_BOARD: 'SELECT_BOARD',
  };

  // ── ACTION CREATORS ──────────────────────────────────────────────
  const actions = {
    addBoard: (title) => ({
      type: ActionTypes.ADD_BOARD,
      payload: { id: 'b_' + Date.now(), title },
    }),
    deleteBoard: (boardId) => ({
      type: ActionTypes.DELETE_BOARD,
      payload: { boardId },
    }),
    selectBoard: (boardId) => ({
      type: ActionTypes.SELECT_BOARD,
      payload: { boardId },
    }),

    addColumn: (boardId, title) => ({
      type: ActionTypes.ADD_COLUMN,
      payload: { boardId, id: 'c_' + Date.now(), title },
    }),
    deleteColumn: (columnId) => ({
      type: ActionTypes.DELETE_COLUMN,
      payload: { columnId },
    }),

    addCard: (columnId, title) => ({
      type: ActionTypes.ADD_CARD,
      payload: { columnId, id: 'card_' + Date.now(), title },
    }),
    editCard: (cardId, updates) => ({
      type: ActionTypes.EDIT_CARD,
      payload: { cardId, updates },
    }),
    deleteCard: (cardId, columnId) => ({
      type: ActionTypes.DELETE_CARD,
      payload: { cardId, columnId },
    }),
    moveCard: (cardId, fromColumnId, toColumnId) => ({
      type: ActionTypes.MOVE_CARD,
      payload: { cardId, fromColumnId, toColumnId },
    }),

    addTag: (name, color) => ({
      type: ActionTypes.ADD_TAG,
      payload: { id: 'tag_' + Date.now(), name, color },
    }),
    deleteTag: (tagId) => ({
      type: ActionTypes.DELETE_TAG,
      payload: { tagId },
    }),

    setTagFilter: (tagIds) => ({
      type: ActionTypes.SET_TAG_FILTER,
      payload: { tagIds },
    }),
    setSearchFilter: (text) => ({
      type: ActionTypes.SET_SEARCH_FILTER,
      payload: { text },
    }),
  };

  // ── INITIAL STATE ────────────────────────────────────────────────
  const initialState = {
    boards: {},
    columns: {},
    cards: {},
    tags: {},
    filters: {
      tagIds: [], // array of selected tag IDs (AND filter)
      searchText: '',
    },
    boardColumnOrder: {}, // { boardId: [colId, colId, ...] }
    columnCardOrder: {}, // { colId: [cardId, cardId, ...] }
    selectedBoardId: null,
    allBoardIds: [],
  };

  // ── SUB-REDUCERS ─────────────────────────────────────────────────

  function boardsReducer(state = {}, action) {
    switch (action.type) {
      case ActionTypes.ADD_BOARD: {
        const { id, title } = action.payload;
        return {
          ...state,
          [id]: { id, title, createdAt: new Date().toISOString() },
        };
      }

      case ActionTypes.DELETE_BOARD: {
        const { boardId } = action.payload;
        const { [boardId]: removed, ...rest } = state;
        return rest;
      }

      default:
        return state;
    }
  }

  function columnsReducer(state = {}, action, boardColumnOrder) {
    switch (action.type) {
      case ActionTypes.ADD_COLUMN: {
        const { id, boardId, title } = action.payload;
        return {
          ...state,
          [id]: { id, title, boardId },
        };
      }

      case ActionTypes.DELETE_COLUMN: {
        const { columnId } = action.payload;
        const { [columnId]: removed, ...rest } = state;
        return rest;
      }

      default:
        return state;
    }
  }

  function cardsReducer(state = {}, action) {
    switch (action.type) {
      case ActionTypes.ADD_CARD: {
        const { id, columnId, title } = action.payload;
        return {
          ...state,
          [id]: {
            id,
            title,
            description: '',
            dueDate: null,
            assignee: null,
            tagIds: [],
            columnId,
            createdAt: new Date().toISOString(),
          },
        };
      }

      case ActionTypes.EDIT_CARD: {
        const { cardId, updates } = action.payload;
        return {
          ...state,
          [cardId]: { ...state[cardId], ...updates },
        };
      }

      case ActionTypes.DELETE_CARD: {
        const { cardId } = action.payload;
        const { [cardId]: removed, ...rest } = state;
        return rest;
      }

      case ActionTypes.MOVE_CARD: {
        const { cardId } = action.payload;
        return {
          ...state,
          [cardId]: {
            ...state[cardId],
            columnId: action.payload.toColumnId,
          },
        };
      }

      default:
        return state;
    }
  }

  function tagsReducer(state = {}, action) {
    switch (action.type) {
      case ActionTypes.ADD_TAG: {
        const { id, name, color } = action.payload;
        return {
          ...state,
          [id]: { id, name, color },
        };
      }

      case ActionTypes.DELETE_TAG: {
        const { tagId } = action.payload;
        const { [tagId]: removed, ...rest } = state;
        return rest;
      }

      default:
        return state;
    }
  }

  // ── ROOT REDUCER ─────────────────────────────────────────────────
  function rootReducer(state, action) {
    let nextState = {
      ...state,
      boards: boardsReducer(state.boards, action),
      columns: columnsReducer(state.columns, action, state.boardColumnOrder),
      cards: cardsReducer(state.cards, action),
      tags: tagsReducer(state.tags, action),
      filters:
        action.type === ActionTypes.SET_TAG_FILTER ||
        action.type === ActionTypes.SET_SEARCH_FILTER ||
        action.type === ActionTypes.CLEAR_FILTERS
          ? filtersReducer(state.filters, action)
          : state.filters,
      selectedBoardId:
        action.type === ActionTypes.SELECT_BOARD
          ? action.payload.boardId
          : state.selectedBoardId,
    };

    // Handle cross-domain side effects
    if (action.type === ActionTypes.DELETE_BOARD) {
      const boardId = action.payload.boardId;
      const columnIds = state.boardColumnOrder[boardId] || [];

      // Remove columns
      const newColumns = { ...nextState.columns };
      columnIds.forEach((colId) => delete newColumns[colId]);
      nextState.columns = newColumns;

      // Remove cards in those columns
      const newCards = { ...nextState.cards };
      Object.values(newCards).forEach((card) => {
        if (columnIds.includes(card.columnId)) {
          delete newCards[card.id];
        }
      });
      nextState.cards = newCards;

      // Clean up orders
      const { [boardId]: removedOrder, ...restOrders } =
        nextState.boardColumnOrder;
      nextState.boardColumnOrder = restOrders;

      // Clean columnCardOrder
      const newColumnCardOrder = { ...nextState.columnCardOrder };
      columnIds.forEach((colId) => delete newColumnCardOrder[colId]);
      nextState.columnCardOrder = newColumnCardOrder;

      // Deselect if needed
      if (nextState.selectedBoardId === boardId) {
        nextState.selectedBoardId = null;
      }
    }

    if (action.type === ActionTypes.DELETE_COLUMN) {
      const columnId = action.payload.columnId;
      const cardIds = state.columnCardOrder[columnId] || [];

      // Remove cards
      const newCards = { ...nextState.cards };
      cardIds.forEach((cardId) => delete newCards[cardId]);
      nextState.cards = newCards;

      // Clean columnCardOrder
      const { [columnId]: removed, ...rest } = nextState.columnCardOrder;
      nextState.columnCardOrder = rest;
    }

    if (action.type === ActionTypes.DELETE_TAG) {
      const tagId = action.payload.tagId;
      const newCards = { ...nextState.cards };
      Object.keys(newCards).forEach((cardId) => {
        newCards[cardId] = {
          ...newCards[cardId],
          tagIds: newCards[cardId].tagIds.filter((id) => id !== tagId),
        };
      });
      nextState.cards = newCards;
    }

    return nextState;
  }

  function filtersReducer(state, action) {
    switch (action.type) {
      case ActionTypes.SET_TAG_FILTER:
        return { ...state, tagIds: action.payload.tagIds };
      case ActionTypes.SET_SEARCH_FILTER:
        return { ...state, searchText: action.payload.text };
      case ActionTypes.CLEAR_FILTERS:
        return { tagIds: [], searchText: '' };
      default:
        return state;
    }
  }

  const [state, dispatch] = React.useReducer(rootReducer, initialState);

  // ── HELPERS ──────────────────────────────────────────────────────
  const getColumnsForBoard = (boardId) => {
    const columnIds = state.boardColumnOrder[boardId] || [];
    return columnIds.map((id) => state.columns[id]).filter(Boolean);
  };

  const getCardsForColumn = (columnId) => {
    const cardIds = state.columnCardOrder[columnId] || [];
    return cardIds
      .map((id) => state.cards[id])
      .filter(Boolean)
      .filter((card) => {
        const matchesSearch = state.filters.searchText
          ? card.title
              .toLowerCase()
              .includes(state.filters.searchText.toLowerCase()) ||
            (card.description || '')
              .toLowerCase()
              .includes(state.filters.searchText.toLowerCase())
          : true;

        const matchesTags =
          state.filters.tagIds.length === 0
            ? true
            : state.filters.tagIds.every((tagId) =>
                card.tagIds.includes(tagId),
              );

        return matchesSearch && matchesTags;
      });
  };

  const selectedBoard = state.selectedBoardId
    ? state.boards[state.selectedBoardId]
    : null;

  // ── UI ───────────────────────────────────────────────────────────
  return (
    <div style={{ padding: '20px', fontFamily: 'system-ui' }}>
      <h1>Kanban Board</h1>

      {/* Board Selector */}
      <div style={{ marginBottom: '24px' }}>
        <h3>Boards</h3>
        <div style={{ display: 'flex', gap: '12px', flexWrap: 'wrap' }}>
          {state.allBoardIds.map((boardId) => {
            const board = state.boards[boardId];
            const isSelected = boardId === state.selectedBoardId;
            return (
              <div
                key={boardId}
                onClick={() => dispatch(actions.selectBoard(boardId))}
                style={{
                  padding: '12px 20px',
                  background: isSelected ? '#3b82f6' : '#f1f5f9',
                  color: isSelected ? 'white' : 'black',
                  borderRadius: '8px',
                  cursor: 'pointer',
                  fontWeight: isSelected ? 'bold' : 'normal',
                }}
              >
                {board.title}
                <button
                  onClick={(e) => {
                    e.stopPropagation();
                    if (
                      window.confirm('Delete this board and all its content?')
                    ) {
                      dispatch(actions.deleteBoard(boardId));
                    }
                  }}
                  style={{
                    marginLeft: '12px',
                    color: isSelected ? '#bfdbfe' : 'red',
                    background: 'none',
                    border: 'none',
                  }}
                >
                  ×
                </button>
              </div>
            );
          })}

          <button
            onClick={() => {
              const title = prompt('Board title:');
              if (title) {
                dispatch(actions.addBoard(title.trim()));
              }
            }}
            style={{
              padding: '12px 20px',
              background: '#10b981',
              color: 'white',
              border: 'none',
              borderRadius: '8px',
            }}
          >
            + New Board
          </button>
        </div>
      </div>

      {selectedBoard && (
        <>
          <h2>{selectedBoard.title}</h2>

          {/* Filters */}
          <div
            style={{
              margin: '16px 0',
              display: 'flex',
              gap: '16px',
              alignItems: 'center',
            }}
          >
            <input
              placeholder='Search cards...'
              value={state.filters.searchText}
              onChange={(e) =>
                dispatch(actions.setSearchFilter(e.target.value))
              }
              style={{
                padding: '8px',
                borderRadius: '6px',
                border: '1px solid #d1d5db',
              }}
            />

            <div>
              Filter by tags:
              {Object.values(state.tags).map((tag) => (
                <label
                  key={tag.id}
                  style={{ marginLeft: '12px' }}
                >
                  <input
                    type='checkbox'
                    checked={state.filters.tagIds.includes(tag.id)}
                    onChange={(e) => {
                      const newTagIds = e.target.checked
                        ? [...state.filters.tagIds, tag.id]
                        : state.filters.tagIds.filter((id) => id !== tag.id);
                      dispatch(actions.setTagFilter(newTagIds));
                    }}
                  />
                  <span
                    style={{
                      background: tag.color,
                      color: 'white',
                      padding: '2px 8px',
                      borderRadius: '12px',
                      marginLeft: '4px',
                    }}
                  >
                    {tag.name}
                  </span>
                </label>
              ))}
            </div>

            <button
              onClick={() => dispatch({ type: ActionTypes.CLEAR_FILTERS })}
            >
              Clear Filters
            </button>

            <button
              onClick={() => {
                const name = prompt('Tag name:');
                const color = prompt('Color (e.g. #ef4444):', '#ef4444');
                if (name && color) {
                  dispatch(actions.addTag(name.trim(), color));
                }
              }}
            >
              + New Tag
            </button>
          </div>

          {/* Columns */}
          <div
            style={{
              display: 'flex',
              gap: '20px',
              overflowX: 'auto',
              paddingBottom: '20px',
            }}
          >
            {getColumnsForBoard(selectedBoard.id).map((column) => (
              <div
                key={column.id}
                style={{
                  background: '#f8fafc',
                  borderRadius: '8px',
                  width: '320px',
                  minHeight: '500px',
                  padding: '12px',
                  boxShadow: '0 1px 3px rgba(0,0,0,0.1)',
                }}
              >
                <div
                  style={{
                    display: 'flex',
                    justifyContent: 'space-between',
                    marginBottom: '12px',
                  }}
                >
                  <h4>{column.title}</h4>
                  <button
                    onClick={() => {
                      if (window.confirm('Delete column and move cards?')) {
                        dispatch({
                          type: ActionTypes.DELETE_COLUMN,
                          payload: { columnId: column.id },
                        });
                      }
                    }}
                    style={{ color: 'red', background: 'none', border: 'none' }}
                  >
                    ×
                  </button>
                </div>

                {/* Cards */}
                {getCardsForColumn(column.id).map((card) => (
                  <div
                    key={card.id}
                    style={{
                      background: 'white',
                      borderRadius: '8px',
                      padding: '12px',
                      marginBottom: '12px',
                      boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
                    }}
                  >
                    <strong>{card.title}</strong>

                    {card.description && (
                      <p
                        style={{
                          margin: '8px 0',
                          fontSize: '0.9em',
                          color: '#4b5563',
                        }}
                      >
                        {card.description}
                      </p>
                    )}

                    {card.tagIds.length > 0 && (
                      <div style={{ margin: '8px 0' }}>
                        {card.tagIds.map((tagId) => {
                          const tag = state.tags[tagId];
                          return tag ? (
                            <span
                              key={tagId}
                              style={{
                                background: tag.color,
                                color: 'white',
                                padding: '2px 8px',
                                borderRadius: '12px',
                                fontSize: '0.8em',
                                marginRight: '6px',
                              }}
                            >
                              {tag.name}
                            </span>
                          ) : null;
                        })}
                      </div>
                    )}

                    <div
                      style={{
                        marginTop: '12px',
                        fontSize: '0.85em',
                        color: '#6b7280',
                      }}
                    >
                      {card.dueDate && (
                        <div>
                          Due: {new Date(card.dueDate).toLocaleDateString()}
                        </div>
                      )}
                      {card.assignee && <div>Assignee: {card.assignee}</div>}
                    </div>

                    <div
                      style={{ marginTop: '12px', display: 'flex', gap: '8px' }}
                    >
                      <button
                        onClick={() => {
                          const title = prompt('New title:', card.title);
                          if (title)
                            dispatch(actions.editCard(card.id, { title }));
                        }}
                      >
                        Edit
                      </button>

                      <button
                        onClick={() => {
                          if (window.confirm('Delete card?')) {
                            dispatch(actions.deleteCard(card.id, column.id));
                          }
                        }}
                        style={{ color: 'red' }}
                      >
                        Delete
                      </button>

                      <select
                        value=''
                        onChange={(e) => {
                          const toColumnId = e.target.value;
                          if (toColumnId) {
                            dispatch(
                              actions.moveCard(card.id, column.id, toColumnId),
                            );
                          }
                        }}
                        style={{ marginLeft: 'auto' }}
                      >
                        <option value=''>Move to...</option>
                        {getColumnsForBoard(selectedBoard.id)
                          .filter((c) => c.id !== column.id)
                          .map((c) => (
                            <option
                              key={c.id}
                              value={c.id}
                            >
                              {c.title}
                            </option>
                          ))}
                      </select>
                    </div>
                  </div>
                ))}

                {/* Add Card */}
                <button
                  onClick={() => {
                    const title = prompt('Card title:');
                    if (title) {
                      dispatch(actions.addCard(column.id, title.trim()));
                    }
                  }}
                  style={{
                    width: '100%',
                    padding: '10px',
                    marginTop: '12px',
                    background: '#e5e7eb',
                    border: 'none',
                    borderRadius: '6px',
                    cursor: 'pointer',
                  }}
                >
                  + Add Card
                </button>
              </div>
            ))}

            {/* Add Column */}
            <button
              onClick={() => {
                const title = prompt('Column title:');
                if (title) {
                  dispatch(actions.addColumn(selectedBoard.id, title.trim()));
                }
              }}
              style={{
                width: '280px',
                height: '120px',
                background: '#f1f5f9',
                border: '2px dashed #9ca3af',
                borderRadius: '8px',
                fontSize: '1.1em',
                cursor: 'pointer',
              }}
            >
              + Add Column
            </button>
          </div>
        </>
      )}

      {!selectedBoard && state.allBoardIds.length === 0 && (
        <p style={{ textAlign: 'center', color: '#6b7280', marginTop: '60px' }}>
          Create your first board to get started
        </p>
      )}
    </div>
  );
}

/*
Kết quả ví dụ:

1. Nhấn "+ New Board" → tạo "Project X"
2. Chọn board → thấy nút "+ Add Column"
3. Tạo 3 cột: To Do, In Progress, Done
4. Trong cột To Do → "+ Add Card" → "Implement login"
5. Thêm tag "Bug" (màu đỏ) → gán tag vào card
6. Tạo card khác → di chuyển card sang "In Progress" qua dropdown
7. Tìm kiếm "login" → chỉ hiện card liên quan
8. Xóa tag "Bug" → tag biến mất khỏi tất cả card
9. Xóa column "Done" → card trong đó bị xóa
10. Xóa board → toàn bộ columns + cards biến mất
*/

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

Bảng So Sánh: Nested vs Normalized State

Tiêu chíNested StateNormalized StateWinner
Setup Complexity✅ Đơn giản, tự nhiên❌ Phức tạp, cần planNested
Update Performance❌ Clone entire tree✅ Clone only changed partNormalized
Query Simplicity✅ Direct access: user.posts❌ Need denormalizeNested
Data Duplication❌ Duplicate data across tree✅ No duplicationNormalized
Deeply Nested Updates❌ Nightmare (4+ levels)✅ Simple (1 level)Normalized
Consistency❌ Same data in multiple places✅ Single source of truthNormalized
Learning Curve✅ Easy to understand❌ Cần hiểu patternNested
Scalability❌ Slow khi data grows✅ Scales wellNormalized
TypeScript Support⚠️ Complex nested types✅ Simple flat typesNormalized
Testing❌ Phải setup entire tree✅ Test isolated partsNormalized

Decision Tree: Nested vs Normalized?

START: Thiết kế state structure?

├─ Data đơn giản (1-2 levels nesting)?
│  └─ YES → Nested State ✅
│     Example: { user: { name, settings: { theme } } }

├─ Data được update thường xuyên?
│  └─ YES → Normalized State ✅
│     Reason: Fast updates quan trọng

├─ Data có relationships phức tạp?
│  (Many-to-many, bidirectional)
│  └─ YES → Normalized State ✅
│     Example: Users ↔ Posts ↔ Comments

├─ Data tree sâu (3+ levels)?
│  └─ YES → Normalized State ✅
│     Reason: Avoid nested update hell

├─ Data có duplication?
│  (Same object in multiple places)
│  └─ YES → Normalized State ✅
│     Example: Author info duplicated in posts

├─ Primarily read-only data?
│  └─ YES → Nested State ✅
│     Example: Config, static content

├─ Need to reference same item từ multiple places?
│  └─ YES → Normalized State ✅
│     Example: Tag referenced by multiple posts

└─ NOT SURE?
   → Start Nested (simple)
   → Refactor to Normalized khi:
      - Updates become slow
      - Code becomes messy
      - Data duplication issues

Action Creators: Pros & Cons

ApproachCodeProsConsUse When
Inline Objectsdispatch({ type: 'ADD', payload: {...} })• Ít code
• Trực quan
• Typo risk
• Duplicate logic
• Hard refactor
Prototype, simple apps
Constantsdispatch({ type: Types.ADD, payload: {...} })• Autocomplete
• Catch typos
• Still duplicate payload logicSmall-medium apps
Action Creatorsdispatch(actions.add(data))• No duplication
• Consistent payload
• Easy test
• Type-safe
• More boilerplateMedium-large apps

Reducer Composition Patterns

Pattern 1: Domain-Based Split

jsx
// ✅ GOOD: Split by domain
rootReducer = {
  users: usersReducer,
  posts: postsReducer,
  comments: commentsReducer,
}

// Use: State shape mirrors reducers
state.users → handled by usersReducer
state.posts → handled by postsReducer

Pattern 2: Feature-Based Split

jsx
// ✅ GOOD: Split by feature
rootReducer = {
  auth: authReducer,
  dashboard: dashboardReducer,
  settings: settingsReducer,
};

// Use: Feature isolation

Pattern 3: Hybrid

jsx
// ✅ GOOD: Domain + Feature
rootReducer = {
  // Shared data
  entities: entitiesReducer, // users, posts, comments

  // Feature-specific
  auth: authReducer,
  ui: uiReducer,
};

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

Bug 1: Incorrect Normalization 🐛

jsx
// ❌ CODE BỊ LỖI
const state = {
  posts: {
    1: {
      id: 1,
      title: 'Hello',
      author: { id: 'user1', name: 'Alice' }, // 🐛 Nested author!
      comments: [
        { id: 'c1', text: 'Nice!' }, // 🐛 Nested comments!
      ],
    },
  },
};

function reducer(state, action) {
  switch (action.type) {
    case 'UPDATE_USER_NAME': {
      // 🐛 Phải update ở nhiều chỗ!
      const newPosts = {};
      Object.keys(state.posts).forEach((postId) => {
        const post = state.posts[postId];
        if (post.author.id === action.userId) {
          newPosts[postId] = {
            ...post,
            author: { ...post.author, name: action.name },
          };
        } else {
          newPosts[postId] = post;
        }
      });
      return { ...state, posts: newPosts };
    }
  }
}

❓ Câu hỏi:

  1. Vấn đề gì với state structure?
  2. Tại sao update user name phức tạp?
  3. Fix như thế nào?

💡 Giải thích:

  • Author nested → duplicated trong mỗi post
  • Update name → phải traverse tất cả posts
  • Comments nested → khó update individual comment

✅ Fix:

jsx
// ✅ Normalized state
const state = {
  users: {
    user1: { id: 'user1', name: 'Alice' },
  },
  posts: {
    1: { id: 1, title: 'Hello', authorId: 'user1', commentIds: ['c1'] },
  },
  comments: {
    c1: { id: 'c1', text: 'Nice!', authorId: 'user1' },
  },
};

function reducer(state, action) {
  switch (action.type) {
    case 'UPDATE_USER_NAME': {
      // ✅ Simple! Update 1 chỗ
      return {
        ...state,
        users: {
          ...state.users,
          [action.userId]: {
            ...state.users[action.userId],
            name: action.name,
          },
        },
      };
    }
  }
}

Bug 2: Broken Cascade Delete 🐛

jsx
// ❌ CODE BỊ LỖI
function reducer(state, action) {
  switch (action.type) {
    case 'DELETE_POST': {
      const { postId } = action.payload;

      // 🐛 Delete post nhưng comments vẫn còn!
      const { [postId]: deleted, ...remainingPosts } = state.posts;

      return {
        ...state,
        posts: remainingPosts,
        // 🐛 Comments orphaned! postCommentIds not cleaned!
      };
    }
  }
}

// State after delete:
// {
//   posts: {}, // Post deleted
//   comments: { 1001: {...}, 1002: {...} }, // Comments still here!
//   postCommentIds: { 101: [1001, 1002] } // Reference still here!
// }

❓ Câu hỏi:

  1. Vấn đề gì xảy ra sau khi delete post?
  2. Memory leak ở đâu?
  3. Fix như thế nào?

💡 Giải thích:

  • Delete post nhưng không delete comments → orphaned data
  • postCommentIds còn reference → memory leak
  • Display UI sẽ error (render comment của post không tồn tại)

✅ Fix:

jsx
function reducer(state, action) {
  switch (action.type) {
    case 'DELETE_POST': {
      const { postId } = action.payload;

      // 1️⃣ Get comments to delete
      const commentIds = state.postCommentIds[postId] || [];

      // 2️⃣ Delete post
      const { [postId]: deletedPost, ...remainingPosts } = state.posts;

      // 3️⃣ Delete comments
      const newComments = { ...state.comments };
      commentIds.forEach((commentId) => {
        delete newComments[commentId];
      });

      // 4️⃣ Delete postCommentIds entry
      const { [postId]: deletedIds, ...remainingPostCommentIds } =
        state.postCommentIds;

      return {
        ...state,
        posts: remainingPosts,
        comments: newComments,
        postCommentIds: remainingPostCommentIds,
      };
    }
  }
}

Bug 3: Action Type Typo 🐛

jsx
// ❌ CODE BỊ LỖI
// actions.js
const ActionTypes = {
  ADD_TODO: 'ADD_TODO',
  TOGGLE_TODO: 'TOGGLE_TODO',
  DELETE_TODO: 'DELETE_TODO',
};

// reducer.js
function reducer(state, action) {
  switch (action.type) {
    case ActionTypes.ADD_TODO: return ...;
    case ActionTypes.TOGGLE_TODO: return ...;
    case 'DELETE_TODO': return ...; // 🐛 Magic string thay vì constant!
  }
}

// component.js
dispatch({ type: 'DELET_TODO', payload: { id: 1 } }); // 🐛 Typo!
// → Hit default case → throw error: "Unknown action: DELET_TODO"
// → Khó debug vì không biết typo ở đâu

❓ Câu hỏi:

  1. Vấn đề gì với code?
  2. Làm sao prevent typos?
  3. Best practice?

💡 Giải thích:

  • Magic strings → typo không catch được
  • Reducer dùng constant, component dùng string → inconsistent
  • Refactor action type name → phải find/replace everywhere

✅ Fix:

jsx
// ✅ ALWAYS use constants + action creators

// constants.js
export const ActionTypes = {
  ADD_TODO: 'ADD_TODO',
  TOGGLE_TODO: 'TOGGLE_TODO',
  DELETE_TODO: 'DELETE_TODO',
};

// actions.js
export const actions = {
  addTodo: (text) => ({
    type: ActionTypes.ADD_TODO,
    payload: { text },
  }),

  toggleTodo: (id) => ({
    type: ActionTypes.TOGGLE_TODO,
    payload: { id },
  }),

  deleteTodo: (id) => ({
    type: ActionTypes.DELETE_TODO,
    payload: { id },
  }),
};

// reducer.js
import { ActionTypes } from './constants';

function reducer(state, action) {
  switch (action.type) {
    case ActionTypes.ADD_TODO: return ...;
    case ActionTypes.TOGGLE_TODO: return ...;
    case ActionTypes.DELETE_TODO: return ...; // ✅ Consistent!
  }
}

// component.js
import { actions } from './actions';

dispatch(actions.deleteTodo(1)); // ✅ Type-safe, no typos!

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

Knowledge Check

Đánh dấu ✅ khi bạn tự tin:

State Normalization:

  • [ ] Tôi hiểu normalized state là gì
  • [ ] Tôi biết khi nào nên normalize
  • [ ] Tôi biết cách normalize nested data
  • [ ] Tôi biết cách denormalize để display
  • [ ] Tôi hiểu trade-offs: nested vs normalized

Action Management:

  • [ ] Tôi dùng action type constants thay vì magic strings
  • [ ] Tôi biết cách tạo action creators
  • [ ] Tôi hiểu lợi ích của action creators
  • [ ] Tôi biết cách organize action types (enum pattern)

Reducer Composition:

  • [ ] Tôi biết khi nào split reducer
  • [ ] Tôi biết cách combine reducers manually
  • [ ] Tôi hiểu domain-based vs feature-based split
  • [ ] Tôi biết cách handle cross-domain actions

Edge Cases:

  • [ ] Tôi biết cách cascade delete trong normalized state
  • [ ] Tôi biết cách handle orphaned data
  • [ ] Tôi biết cách update relationships (many-to-many)

Code Review Checklist

State Structure:

  • [ ] Flat structure cho large/complex data
  • [ ] Entities keyed by ID (object, not array)
  • [ ] No duplication (references by ID)
  • [ ] Separate ordering arrays (allIds) nếu cần

Actions:

  • [ ] All action types là constants
  • [ ] Action creators cho complex payloads
  • [ ] Payload structures consistent
  • [ ] Action creators documented

Reducers:

  • [ ] Each reducer < 150 lines
  • [ ] Clear domain boundaries
  • [ ] Cross-domain logic documented
  • [ ] Cascade deletes handled
  • [ ] Immutable updates

Code Quality:

  • [ ] No magic strings
  • [ ] Clear naming (ActionTypes, actions, reducers)
  • [ ] Comments cho complex logic
  • [ ] Helper functions extracted

🏠 BÀI TẬP VỀ NHÀ

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

Bài 1: Normalize Nested State

Cho nested state sau:

jsx
const nestedState = {
  categories: [
    {
      id: 1,
      name: 'Electronics',
      products: [
        {
          id: 101,
          name: 'iPhone',
          price: 999,
          reviews: [
            {
              id: 1001,
              rating: 5,
              text: 'Great!',
              user: { id: 'u1', name: 'Alice' },
            },
            {
              id: 1002,
              rating: 4,
              text: 'Good',
              user: { id: 'u2', name: 'Bob' },
            },
          ],
        },
        {
          id: 102,
          name: 'iPad',
          price: 799,
          reviews: [],
        },
      ],
    },
    {
      id: 2,
      name: 'Books',
      products: [
        {
          id: 201,
          name: 'React Book',
          price: 49,
          reviews: [
            {
              id: 2001,
              rating: 5,
              text: 'Must read!',
              user: { id: 'u1', name: 'Alice' },
            },
          ],
        },
      ],
    },
  ],
};

Nhiệm vụ:

  1. Design normalized state structure
  2. Write reducer với actions:
    • ADD_REVIEW (thêm review vào product)
    • UPDATE_PRODUCT_PRICE
    • DELETE_CATEGORY (cascade delete products & reviews)
  3. Write helper function: getProductWithReviews(productId)
💡 Solution
jsx
/**
 * Bài tập về nhà - Normalize Nested State (Categories → Products → Reviews)
 *
 * 1. Normalized state structure
 * 2. Reducer hỗ trợ:
 *    - ADD_REVIEW
 *    - UPDATE_PRODUCT_PRICE
 *    - DELETE_CATEGORY (cascade delete products & reviews)
 * 3. Helper: getProductWithReviews(productId)
 *
 * @returns {JSX.Element}
 */
function NormalizedEcommerceDemo() {
  // ── Action Type Constants ────────────────────────────────────────
  const ActionTypes = {
    ADD_REVIEW: 'ADD_REVIEW',
    UPDATE_PRODUCT_PRICE: 'UPDATE_PRODUCT_PRICE',
    DELETE_CATEGORY: 'DELETE_CATEGORY',
  };

  // ── Action Creators ──────────────────────────────────────────────
  const actions = {
    addReview: (productId, rating, text, userId) => ({
      type: ActionTypes.ADD_REVIEW,
      payload: {
        id: 'r_' + Date.now(),
        productId,
        rating,
        text,
        userId,
        createdAt: new Date().toISOString(),
      },
    }),

    updateProductPrice: (productId, newPrice) => ({
      type: ActionTypes.UPDATE_PRODUCT_PRICE,
      payload: { productId, newPrice },
    }),

    deleteCategory: (categoryId) => ({
      type: ActionTypes.DELETE_CATEGORY,
      payload: { categoryId },
    }),
  };

  // ── Normalized Initial State ─────────────────────────────────────
  const initialState = {
    categories: {
      1: { id: '1', name: 'Electronics' },
      2: { id: '2', name: 'Books' },
    },
    products: {
      101: { id: '101', name: 'iPhone', price: 999, categoryId: '1' },
      102: { id: '102', name: 'iPad', price: 799, categoryId: '1' },
      201: { id: '201', name: 'React Book', price: 49, categoryId: '2' },
    },
    reviews: {},
    categoryProductIds: {
      1: ['101', '102'],
      2: ['201'],
    },
    productReviewIds: {
      101: [],
      102: [],
      201: [],
    },
    users: {
      u1: { id: 'u1', name: 'Alice' },
      u2: { id: 'u2', name: 'Bob' },
    },
  };

  // ── Reducer ──────────────────────────────────────────────────────
  const reducer = (state, action) => {
    switch (action.type) {
      case ActionTypes.ADD_REVIEW: {
        const { id, productId, rating, text, userId, createdAt } =
          action.payload;

        return {
          ...state,
          reviews: {
            ...state.reviews,
            [id]: { id, productId, rating, text, userId, createdAt },
          },
          productReviewIds: {
            ...state.productReviewIds,
            [productId]: [...(state.productReviewIds[productId] || []), id],
          },
        };
      }

      case ActionTypes.UPDATE_PRODUCT_PRICE: {
        const { productId, newPrice } = action.payload;

        return {
          ...state,
          products: {
            ...state.products,
            [productId]: {
              ...state.products[productId],
              price: newPrice,
            },
          },
        };
      }

      case ActionTypes.DELETE_CATEGORY: {
        const { categoryId } = action.payload;

        // 1. Lấy tất cả product IDs trong category
        const productIds = state.categoryProductIds[categoryId] || [];

        // 2. Xóa các reviews của các product đó
        const newReviews = { ...state.reviews };
        const newProductReviewIds = { ...state.productReviewIds };
        productIds.forEach((productId) => {
          const reviewIds = state.productReviewIds[productId] || [];
          reviewIds.forEach((reviewId) => delete newReviews[reviewId]);
          delete newProductReviewIds[productId];
        });

        // 3. Xóa các products
        const newProducts = { ...state.products };
        productIds.forEach((productId) => delete newProducts[productId]);

        // 4. Xóa category và mối quan hệ
        const { [categoryId]: removedCategory, ...restCategories } =
          state.categories;
        const { [categoryId]: removedProductIds, ...restCategoryProductIds } =
          state.categoryProductIds;

        return {
          ...state,
          categories: restCategories,
          products: newProducts,
          reviews: newReviews,
          categoryProductIds: restCategoryProductIds,
          productReviewIds: newProductReviewIds,
        };
      }

      default:
        return state;
    }
  };

  const [state, dispatch] = React.useReducer(reducer, initialState);

  // ── Helper: Denormalize product + reviews ────────────────────────
  const getProductWithReviews = (productId) => {
    const product = state.products[productId];
    if (!product) return null;

    const reviewIds = state.productReviewIds[productId] || [];
    const reviews = reviewIds.map((id) => {
      const review = state.reviews[id];
      return {
        ...review,
        user: state.users[review.userId],
      };
    });

    const category = state.categories[product.categoryId];

    return {
      ...product,
      category,
      reviews,
    };
  };

  // ── UI Demo ──────────────────────────────────────────────────────
  return (
    <div style={{ fontFamily: 'system-ui', padding: '20px' }}>
      <h2>Normalized E-commerce State Demo</h2>

      <div
        style={{
          margin: '20px 0',
          display: 'flex',
          gap: '16px',
          flexWrap: 'wrap',
        }}
      >
        {/* Hiển thị tất cả products */}
        {Object.values(state.products).map((product) => {
          const fullProduct = getProductWithReviews(product.id);
          return (
            <div
              key={product.id}
              style={{
                border: '1px solid #d1d5db',
                borderRadius: '8px',
                padding: '16px',
                width: '320px',
              }}
            >
              <h4>{fullProduct.name}</h4>
              <p>Category: {fullProduct.category?.name}</p>
              <p>
                Price: <strong>${fullProduct.price}</strong>
              </p>

              <button
                onClick={() => {
                  const newPrice = prompt('New price:', fullProduct.price);
                  if (newPrice && !isNaN(newPrice)) {
                    dispatch(
                      actions.updateProductPrice(product.id, Number(newPrice)),
                    );
                  }
                }}
              >
                Update Price
              </button>

              <div style={{ marginTop: '16px' }}>
                <h5>Reviews ({fullProduct.reviews.length})</h5>
                {fullProduct.reviews.map((review) => (
                  <div
                    key={review.id}
                    style={{ margin: '8px 0', fontSize: '0.95em' }}
                  >
                    <strong>{review.user.name}</strong> ({review.rating}★):{' '}
                    {review.text}
                  </div>
                ))}

                <button
                  onClick={() => {
                    const text = prompt('Your review:');
                    const rating = prompt('Rating (1-5):');
                    if (
                      text &&
                      rating &&
                      ['1', '2', '3', '4', '5'].includes(rating)
                    ) {
                      dispatch(
                        actions.addReview(
                          product.id,
                          Number(rating),
                          text,
                          Math.random() > 0.5 ? 'u1' : 'u2', // random user
                        ),
                      );
                    }
                  }}
                  style={{ marginTop: '8px' }}
                >
                  Add Review
                </button>
              </div>
            </div>
          );
        })}
      </div>

      <div style={{ marginTop: '40px' }}>
        <h3>Actions</h3>
        <button
          onClick={() => {
            if (
              window.confirm(
                'Delete Electronics category and all its products & reviews?',
              )
            ) {
              dispatch(actions.deleteCategory('1'));
            }
          }}
          style={{
            background: '#ef4444',
            color: 'white',
            padding: '10px 16px',
            border: 'none',
            borderRadius: '6px',
          }}
        >
          Delete Electronics Category
        </button>
      </div>

      <pre
        style={{
          marginTop: '40px',
          background: '#f8f9fa',
          padding: '16px',
          borderRadius: '8px',
        }}
      >
        {JSON.stringify(state, null, 2)}
      </pre>
    </div>
  );
}

/*
Kết quả ví dụ:

Ban đầu:
- iPhone $999, 0 reviews
- iPad $799, 0 reviews
- React Book $49, 0 reviews

Thao tác:
1. Update price iPhone → 1099 → giá thay đổi ngay lập tức
2. Add review cho iPhone: "Great phone!" 5★ → review xuất hiện
3. Add review khác cho iPhone → danh sách reviews tăng
4. Delete category "Electronics" → iPhone & iPad + tất cả reviews của chúng biến mất
   React Book vẫn còn vì thuộc category khác
*/

Nâng cao (60 phút)

Bài 2: Build Reddit-like Comment System

Requirements:

  1. Posts có nested comments (unlimited depth)
  2. Comments có replies (cũng là comments)
  3. User có thể upvote/downvote comments
  4. Display comment tree with indentation

State structure gợi ý:

jsx
{
  posts: { [id]: { id, title, content, commentIds } },
  comments: {
    [id]: {
      id,
      text,
      authorId,
      postId,
      parentId, // null nếu top-level, commentId nếu reply
      replyIds: [], // Array of reply comment IDs
      upvotes: 0,
      downvotes: 0
    }
  },
  users: { [id]: { id, name } }
}

Actions:

  • ADD_COMMENT (có thể là top-level hoặc reply)
  • EDIT_COMMENT
  • DELETE_COMMENT (cascade delete replies)
  • UPVOTE_COMMENT
  • DOWNVOTE_COMMENT

Challenges:

  • Nested comments display (recursive component)
  • Cascade delete với unlimited depth
  • Calculate total replies count
💡 Solution
jsx
/**
 * Bài tập về nhà - Reddit-like Comment System (Nested comments, unlimited depth)
 *
 * Features:
 * - Posts có comments + replies (recursive)
 * - Upvote / Downvote comments
 * - Add comment (top-level hoặc reply)
 * - Edit comment
 * - Delete comment (cascade delete replies)
 * - Hiển thị comment tree với indentation
 *
 * @returns {JSX.Element}
 */
function RedditCommentsDemo() {
  // ── Action Type Constants ────────────────────────────────────────
  const ActionTypes = {
    ADD_COMMENT: 'ADD_COMMENT',
    EDIT_COMMENT: 'EDIT_COMMENT',
    DELETE_COMMENT: 'DELETE_COMMENT',
    UPVOTE_COMMENT: 'UPVOTE_COMMENT',
    DOWNVOTE_COMMENT: 'DOWNVOTE_COMMENT',
  };

  // ── Action Creators ──────────────────────────────────────────────
  const actions = {
    addComment: (postId, parentId, text, authorId = 'u1') => ({
      type: ActionTypes.ADD_COMMENT,
      payload: {
        id: 'c_' + Date.now(),
        postId,
        parentId: parentId || null,
        text,
        authorId,
        timestamp: new Date().toISOString(),
        upvotes: 0,
        downvotes: 0,
      },
    }),

    editComment: (commentId, newText) => ({
      type: ActionTypes.EDIT_COMMENT,
      payload: { commentId, newText },
    }),

    deleteComment: (commentId) => ({
      type: ActionTypes.DELETE_COMMENT,
      payload: { commentId },
    }),

    upvoteComment: (commentId) => ({
      type: ActionTypes.UPVOTE_COMMENT,
      payload: { commentId },
    }),

    downvoteComment: (commentId) => ({
      type: ActionTypes.DOWNVOTE_COMMENT,
      payload: { commentId },
    }),
  };

  // ── Initial State (Normalized) ───────────────────────────────────
  const initialState = {
    posts: {
      p1: {
        id: 'p1',
        title: 'React vs Vue in 2026 – which one are you using?',
        content: 'Let’s discuss the current state of these frameworks...',
        commentIds: [],
      },
    },
    comments: {}, // { commentId: { id, text, authorId, postId, parentId, replyIds, upvotes, downvotes, timestamp } }
    users: {
      u1: { id: 'u1', name: 'Alice', avatar: '👩' },
      u2: { id: 'u2', name: 'Bob', avatar: '👨' },
      u3: { id: 'u3', name: 'Charlie', avatar: '🧑' },
    },
  };

  // ── Reducer ──────────────────────────────────────────────────────
  const reducer = (state, action) => {
    switch (action.type) {
      case ActionTypes.ADD_COMMENT: {
        const { id, postId, parentId, text, authorId, timestamp } =
          action.payload;

        const newComment = {
          id,
          postId,
          parentId,
          text,
          authorId,
          timestamp,
          upvotes: 0,
          downvotes: 0,
          replyIds: [],
        };

        // Thêm vào comments
        let newState = {
          ...state,
          comments: {
            ...state.comments,
            [id]: newComment,
          },
        };

        // Cập nhật replyIds của parent (nếu là reply)
        if (parentId) {
          newState = {
            ...newState,
            comments: {
              ...newState.comments,
              [parentId]: {
                ...newState.comments[parentId],
                replyIds: [...(newState.comments[parentId].replyIds || []), id],
              },
            },
          };
        }
        // Cập nhật commentIds của post (nếu là top-level)
        else {
          newState = {
            ...newState,
            posts: {
              ...newState.posts,
              [postId]: {
                ...newState.posts[postId],
                commentIds: [...(newState.posts[postId].commentIds || []), id],
              },
            },
          };
        }

        return newState;
      }

      case ActionTypes.EDIT_COMMENT: {
        const { commentId, newText } = action.payload;
        return {
          ...state,
          comments: {
            ...state.comments,
            [commentId]: {
              ...state.comments[commentId],
              text: newText,
              editedAt: new Date().toISOString(),
            },
          },
        };
      }

      case ActionTypes.DELETE_COMMENT: {
        const { commentId } = action.payload;

        // Thu thập tất cả comment IDs cần xóa (cascade)
        const toDelete = new Set([commentId]);
        const queue = [commentId];

        while (queue.length > 0) {
          const current = queue.shift();
          const comment = state.comments[current];
          if (comment?.replyIds) {
            comment.replyIds.forEach((rid) => {
              if (!toDelete.has(rid)) {
                toDelete.add(rid);
                queue.push(rid);
              }
            });
          }
        }

        // Xóa comments
        const newComments = { ...state.comments };
        toDelete.forEach((id) => delete newComments[id]);

        // Cập nhật parent (nếu có)
        const parentId = state.comments[commentId]?.parentId;
        if (parentId) {
          newComments[parentId] = {
            ...newComments[parentId],
            replyIds: (newComments[parentId].replyIds || []).filter(
              (id) => !toDelete.has(id),
            ),
          };
        }
        // Cập nhật post (nếu top-level)
        else {
          const postId = state.comments[commentId]?.postId;
          if (postId) {
            const post = state.posts[postId];
            state.posts[postId] = {
              ...post,
              commentIds: (post.commentIds || []).filter(
                (id) => !toDelete.has(id),
              ),
            };
          }
        }

        return {
          ...state,
          comments: newComments,
        };
      }

      case ActionTypes.UPVOTE_COMMENT:
      case ActionTypes.DOWNVOTE_COMMENT: {
        const { commentId } = action.payload;
        const field =
          action.type === ActionTypes.UPVOTE_COMMENT ? 'upvotes' : 'downvotes';

        return {
          ...state,
          comments: {
            ...state.comments,
            [commentId]: {
              ...state.comments[commentId],
              [field]: (state.comments[commentId][field] || 0) + 1,
            },
          },
        };
      }

      default:
        return state;
    }
  };

  const [state, dispatch] = React.useReducer(reducer, initialState);

  // ── Recursive Comment Component ──────────────────────────────────
  function Comment({ commentId, depth = 0 }) {
    const comment = state.comments[commentId];
    if (!comment) return null;

    const author = state.users[comment.authorId];
    const score = comment.upvotes - comment.downvotes;

    return (
      <div style={{ marginLeft: `${depth * 24}px`, marginBottom: '16px' }}>
        <div
          style={{
            borderLeft: depth > 0 ? '2px solid #d1d5db' : 'none',
            paddingLeft: depth > 0 ? '16px' : '0',
          }}
        >
          <div
            style={{
              display: 'flex',
              alignItems: 'center',
              gap: '8px',
              marginBottom: '4px',
            }}
          >
            <strong>{author.name}</strong>
            <span style={{ color: '#6b7280', fontSize: '0.85em' }}>
              •{' '}
              {new Date(comment.timestamp).toLocaleString([], {
                dateStyle: 'short',
                timeStyle: 'short',
              })}
              {comment.editedAt && ' (edited)'}
            </span>
          </div>

          <p style={{ margin: '4px 0 8px' }}>{comment.text}</p>

          <div
            style={{
              display: 'flex',
              gap: '16px',
              fontSize: '0.9em',
              color: '#4b5563',
            }}
          >
            <button onClick={() => dispatch(actions.upvoteComment(commentId))}>
              ▲ {comment.upvotes}
            </button>
            <button
              onClick={() => dispatch(actions.downvoteComment(commentId))}
            >
              ▼ {comment.downvotes}
            </button>
            <span>Score: {score}</span>

            <button
              onClick={() => {
                const text = prompt('Reply:', '');
                if (text?.trim()) {
                  dispatch(
                    actions.addComment(comment.postId, comment.id, text.trim()),
                  );
                }
              }}
            >
              Reply
            </button>

            <button
              onClick={() => {
                const newText = prompt('Edit comment:', comment.text);
                if (newText?.trim() && newText !== comment.text) {
                  dispatch(actions.editComment(comment.id, newText.trim()));
                }
              }}
            >
              Edit
            </button>

            <button
              onClick={() => {
                if (window.confirm('Delete this comment and all replies?')) {
                  dispatch(actions.deleteComment(comment.id));
                }
              }}
              style={{ color: '#ef4444' }}
            >
              Delete
            </button>
          </div>

          {/* Replies */}
          {comment.replyIds?.length > 0 && (
            <div style={{ marginTop: '12px' }}>
              {comment.replyIds.map((replyId) => (
                <Comment
                  key={replyId}
                  commentId={replyId}
                  depth={depth + 1}
                />
              ))}
            </div>
          )}
        </div>
      </div>
    );
  }

  // ── UI ───────────────────────────────────────────────────────────
  const post = state.posts.p1;
  const topLevelComments = (post.commentIds || [])
    .map((id) => state.comments[id])
    .filter(Boolean);

  return (
    <div
      style={{
        maxWidth: '800px',
        margin: '0 auto',
        padding: '20px',
        fontFamily: 'system-ui',
      }}
    >
      <h1>{post.title}</h1>
      <p style={{ color: '#4b5563' }}>{post.content}</p>

      <div style={{ marginTop: '32px' }}>
        <h3>Comments ({topLevelComments.length})</h3>

        <div style={{ margin: '16px 0' }}>
          <textarea
            id='topComment'
            placeholder='What are your thoughts?'
            style={{
              width: '100%',
              minHeight: '80px',
              padding: '12px',
              borderRadius: '8px',
              border: '1px solid #d1d5db',
            }}
          />
          <button
            onClick={() => {
              const input = document.getElementById('topComment');
              if (input.value.trim()) {
                dispatch(actions.addComment('p1', null, input.value.trim()));
                input.value = '';
              }
            }}
            style={{
              marginTop: '8px',
              padding: '8px 16px',
              background: '#3b82f6',
              color: 'white',
              border: 'none',
              borderRadius: '6px',
              cursor: 'pointer',
            }}
          >
            Comment
          </button>
        </div>

        <div>
          {topLevelComments.map((comment) => (
            <Comment
              key={comment.id}
              commentId={comment.id}
              depth={0}
            />
          ))}
        </div>

        {topLevelComments.length === 0 && (
          <p
            style={{ color: '#6b7280', textAlign: 'center', marginTop: '40px' }}
          >
            Be the first to comment!
          </p>
        )}
      </div>
    </div>
  );
}

/*
Kết quả ví dụ:

1. Gõ "Vue still has better DX in 2026" → Comment → xuất hiện top-level comment
2. Nhấn Reply dưới comment → gõ "Nah, React Server Components win" → reply xuất hiện thụt vào
3. Reply tiếp dưới reply → tạo depth 3
4. Upvote / Downvote → score thay đổi
5. Edit comment → text cập nhật + (edited)
6. Delete comment ở depth 2 → toàn bộ subtree (nếu có replies) bị xóa
*/

📚 TÀI LIỆU THAM KHẢO

Bắt buộc đọc

  1. Normalizing State Shape:

  2. React Docs - Extracting State Logic:

Đọc thêm

  1. Immutable Update Patterns:

  2. Reducer Composition:


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

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

  • Ngày 26: useReducer Fundamentals

    • Hôm nay build trên nền đó: advanced patterns
    • Reducer basics → Now: complex state structures
  • Ngày 1-2: ES6 Destructuring, Spread

    • Critical cho immutable updates
    • const { [id]: deleted, ...rest } = state.items
  • Ngày 31-34: Performance (memo, useMemo)

    • Normalized state → fast updates
    • Denormalize helpers → useMemo

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

  • Ngày 28: useReducer + useEffect

    • Data fetching patterns
    • Normalize API responses
    • Loading/error states trong reducer
  • Ngày 29: Custom Hooks với useReducer

    • useNormalizedData hook
    • useEntityManager hook
    • Reusable state management
  • Phase 5: Context API

    • Context + useReducer = Global state
    • Alternative to Redux
    • Normalized state globally

💡 SENIOR INSIGHTS

Cân Nhắc Production

1. Khi nào KHÔNG normalize:

jsx
// ✅ GOOD: Nested OK cho read-only config
const themeConfig = {
  light: {
    colors: { primary: '#007bff', secondary: '#6c757d' },
    fonts: { heading: 'Arial', body: 'Helvetica' },
  },
  dark: {
    /* ... */
  },
};

// ❌ BAD: Normalize sẽ over-engineering
// {
//   themes: { light: {...}, dark: {...} },
//   colors: { primary_light: '#007bff', ... },
//   fonts: { heading_light: 'Arial', ... }
// }

2. Normalization levels:

jsx
// Level 1: Flat entities
{ users: {...}, posts: {...}, comments: {...} }

// Level 2: Relationships tracked
{ users: {...}, posts: {...}, userPostIds: {...} }

// Level 3: Full relational (như database)
// with indexes, lookups, etc.

// → Chọn level phù hợp với complexity

3. Migration strategy:

jsx
// ❌ DON'T: Refactor everything at once
// ✅ DO: Migrate gradually

// Step 1: Add normalized entities alongside nested
const state = {
  // Old (keep working)
  users: [...nested...],

  // New (add gradually)
  normalizedUsers: {...},
};

// Step 2: Migrate reads to normalized
// Step 3: Migrate writes
// Step 4: Remove old structure

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

Junior Level:

  1. Q: "Normalized state là gì?"

    Expected:

    • Flat structure, no nesting
    • Entities keyed by ID
    • References by ID thay vì embedding
    • Example: posts reference userId, không embed user object
  2. Q: "Tại sao dùng action creators?"

    Expected:

    • Prevent typos
    • Centralize logic
    • Consistent payload
    • Easy to refactor

Mid Level:

  1. Q: "Làm sao handle cascade delete trong normalized state?"

    Expected:

    • Identify related entities (postCommentIds)
    • Delete main entity
    • Loop và delete related entities
    • Clean up relationship arrays
    • Example code
  2. Q: "Trade-off của normalized state?"

    Expected:

    • Pros: Fast updates, no duplication, scalable
    • Cons: Complex queries, need denormalize, boilerplate
    • When to use: Large data, frequent updates, relationships
    • When not: Simple data, read-only

Senior Level:

  1. Q: "Thiết kế state architecture cho social network app (users, posts, comments, likes, shares). Explain your decisions."

    Expected:

    • Normalized structure với entities
    • Separate metadata (counts, timestamps)
    • Consider read/write patterns
    • Indexes cho common queries
    • Pagination strategy
    • Caching denormalized data
    • Performance tradeoffs
    • Migration path

War Stories

Story 1: Nested Hell → Normalized Heaven

"App e-commerce có products nested trong categories, reviews nested trong products. Update product price phải clone entire category tree. Performance terrible với 1000+ products. Sau 2 ngày refactor sang normalized: state.products[id].price = X. Update time từ 150ms → 5ms. Code từ 50 lines nested spread → 10 lines flat. Lesson: Profile before optimize, nhưng normalized thường win." - Senior Engineer

Story 2: Premature Normalization

"Junior dev normalize TẤT CẢ, kể cả user settings object (3 fields). Kết quả: Simple feature cần 5 actions, 3 reducers, 10 helper functions. Code review: 'This is 2 levels deep, nested is fine'. Rolled back, code giảm 70%. Lesson: Normalize khi CẦN, không phải by default." - Tech Lead

Story 3: Action Creator Saved Us

"Production bug: Feature flag typo 'ENABEL_FEATURE' → silent fail trong switch/case, users không thấy feature. Với action creator, typo = autocomplete error ngay. Sau đó enforce rule: NO dispatch inline objects. Only action creators. Bugs giảm 40%." - CTO


🎯 PREVIEW NGÀY MAI

Ngày 28: useReducer + useEffect - Async Actions Pattern

Bạn sẽ học:

  • ✨ Data fetching với useReducer
  • ✨ Loading/Success/Error states pattern
  • ✨ Request cancellation
  • ✨ Optimistic updates
  • ✨ Retry logic

Chuẩn bị:

  • Hoàn thành bài tập hôm nay (normalize state)
  • Review useEffect (Ngày 16-20)
  • Review data fetching basics
  • Suy nghĩ: Làm sao combine reducer (sync) + effects (async)?

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

Bạn giờ đã master:

  • ✅ State normalization
  • ✅ Action type constants & creators
  • ✅ Reducer composition
  • ✅ Complex state architectures

Tomorrow: Async world với useReducer! 💪

Personal tech knowledge base