Skip to content

📅 NGÀY 14: Lifting State Up

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

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

  • [ ] Hiểu khi nào cần lift state up vs khi nào giữ state local
  • [ ] Share state giữa sibling components thông qua parent
  • [ ] Implement inverse data flow (child → parent communication via callbacks)
  • [ ] Nhận biết và giải quyết props drilling một cách hợp lý
  • [ ] Thiết kế component hierarchy với state placement tối ưu

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

Trả lời 3 câu hỏi sau để kích hoạt kiến thức từ Ngày 11-13:

  1. Câu 1: State được define trong component A. Component B (con của A) có thể access state đó không? Làm thế nào?

  2. Câu 2: Hai sibling components cần share data. Nên đặt state ở đâu?

  3. Câu 3: Code này có vấn đề gì?

jsx
function Parent() {
  return <Child />;
}

function Child() {
  const [count, setCount] = useState(0);
  // Parent muốn biết count value - làm thế nào?
}
💡 Xem đáp án
  1. Component B có thể access qua props. Parent (A) pass state xuống: <B value={state} />
  2. State nên đặt ở parent chung gần nhất (closest common ancestor)
  3. Data flow một chiều: Child không thể tự send data lên Parent. Cần callback prop: <Child onCountChange={handleChange} />

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

1.1 Vấn Đề Thực Tế

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

jsx
// ❌ PROBLEM: Hai components cần cùng data nhưng state isolated
function ProductList() {
  const [selectedProduct, setSelectedProduct] = useState(null);

  return (
    <div>
      <h2>Products</h2>
      {/* List of products, click to select */}
    </div>
  );
}

function ProductDetails() {
  // ❌ Làm sao biết product nào đang selected???
  // selectedProduct ở ProductList, không access được!

  return (
    <div>
      <h2>Details</h2>
      {/* Show selected product details */}
    </div>
  );
}

function App() {
  return (
    <div>
      <ProductList />
      <ProductDetails />
    </div>
  );
}

Problems:

  • ProductList và ProductDetails là siblings (cùng level)
  • Không component nào pass props cho nhau được
  • State trong ProductList không thể access từ ProductDetails
  • Cần share state giữa siblings!

1.2 Giải Pháp: Lifting State Up

Core Principle:

"Khi 2+ components cần share state, lift state lên parent chung gần nhất của chúng"

┌─────────────────────────────────────────┐
│          LIFTING STATE UP               │
├─────────────────────────────────────────┤
│                                         │
│  BEFORE:                                │
│  ┌──────────────┐  ┌──────────────┐     │
│  │ Component A  │  │ Component B  │     │
│  │ [state] ❌   │  │ needs state  │     │
│  └──────────────┘  └──────────────┘     │
│         ↑                ↑              │
│         └────────┬───────┘              │
│              Parent                     │
│                                         │
│  AFTER:                                 │
│              Parent                     │
│             [state] ✅                  │
│         ↓            ↓                  │
│  ┌──────────────┐  ┌──────────────┐     │
│  │ Component A  │  │ Component B  │     │
│  │ gets props   │  │ gets props   │     │
│  └──────────────┘  └──────────────┘     │
│                                         │
│  State "lifted up" to parent            │
│  Both children receive via props        │
└─────────────────────────────────────────┘

1.3 Mental Model

Analogy: Shared Whiteboard

❌ BAD: Mỗi người có whiteboard riêng
Person A (whiteboard A) → Person B không thấy
Person B (whiteboard B) → Person A không thấy
[Không sync được!]

✅ GOOD: Dùng chung 1 whiteboard ở giữa
       Shared Whiteboard
           ↙        ↘
      Person A    Person B
[Cả 2 đều thấy và update cùng data!]

React Flow:

Parent Component (state owner)
    ↓                    ↓
Child A            Child B
(reads via props)  (reads via props)
    ↓                    ↓
onChange callback → update parent state
    ↓                    ↓
Parent state changes
    ↓                    ↓
Both children re-render with new props

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

Myth 1: "Lifting state = always lift to App component"
Truth: Chỉ lift đến parent gần nhất, không cao hơn cần thiết

Myth 2: "State càng cao càng tốt"
Truth: State càng gần nơi dùng càng tốt. Chỉ lift khi CẦN share

Myth 3: "Child không thể update parent state"
Truth: Child CÓ THỂ update qua callback props

Myth 4: "Lifting state làm app chậm"
Truth: Nếu lift đúng chỗ thì OK. Lift quá cao mới chậm (re-render không cần thiết components)


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

Demo 1: Basic Lifting - Chuyển đổi Độ C sang Độ F ⭐

❌ CÁCH SAI: State Isolated

jsx
// ❌ PROBLEM: Celsius và Fahrenheit không sync
function CelsiusInput() {
  const [temperature, setTemperature] = useState('');

  return (
    <div>
      <label>Celsius:</label>
      <input
        value={temperature}
        onChange={(e) => setTemperature(e.target.value)}
      />
    </div>
  );
}

function FahrenheitInput() {
  const [temperature, setTemperature] = useState('');

  return (
    <div>
      <label>Fahrenheit:</label>
      <input
        value={temperature}
        onChange={(e) => setTemperature(e.target.value)}
      />
    </div>
  );
}

function TemperatureConverter() {
  return (
    <div>
      <CelsiusInput />
      <FahrenheitInput />
      {/* ❌ Gõ vào Celsius, Fahrenheit không update */}
      {/* ❌ 2 states riêng biệt, không sync! */}
    </div>
  );
}

Problems:

  • Mỗi input có state riêng
  • Không cách nào sync giữa chúng
  • User gõ vào 1 field, field kia không update

✅ CÁCH ĐÚNG: Lift State Up

jsx
// Helper functions
function toCelsius(fahrenheit) {
  return ((fahrenheit - 32) * 5) / 9;
}

function toFahrenheit(celsius) {
  return (celsius * 9) / 5 + 32;
}

// ✅ Child components nhận props thay vì có state riêng
function TemperatureInput({ scale, temperature, onTemperatureChange }) {
  return (
    <div style={{ marginBottom: '10px' }}>
      <label style={{ display: 'inline-block', width: '100px' }}>
        {scale === 'c' ? 'Celsius:' : 'Fahrenheit:'}
      </label>
      <input
        value={temperature}
        onChange={(e) => onTemperatureChange(e.target.value)}
        style={{ padding: '5px', width: '200px' }}
      />
    </div>
  );
}

// ✅ Parent owns the state
function TemperatureConverter() {
  const [temperature, setTemperature] = useState('');
  const [scale, setScale] = useState('c'); // 'c' or 'f'

  const handleCelsiusChange = (temp) => {
    setTemperature(temp);
    setScale('c');
  };

  const handleFahrenheitChange = (temp) => {
    setTemperature(temp);
    setScale('f');
  };

  // ✅ Calculate derived values
  const celsius =
    scale === 'f'
      ? toCelsius(parseFloat(temperature))
      : parseFloat(temperature);
  const fahrenheit =
    scale === 'c'
      ? toFahrenheit(parseFloat(temperature))
      : parseFloat(temperature);

  return (
    <div style={{ padding: '20px', fontFamily: 'system-ui' }}>
      <h2>🌡️ Temperature Converter</h2>

      <TemperatureInput
        scale='c'
        temperature={scale === 'c' ? temperature : celsius.toFixed(1)}
        onTemperatureChange={handleCelsiusChange}
      />

      <TemperatureInput
        scale='f'
        temperature={scale === 'f' ? temperature : fahrenheit.toFixed(1)}
        onTemperatureChange={handleFahrenheitChange}
      />

      <div
        style={{
          marginTop: '20px',
          padding: '15px',
          background: '#f0f0f0',
          borderRadius: '4px',
        }}
      >
        {temperature && !isNaN(celsius) ? (
          <p>
            <strong>Result:</strong> {celsius.toFixed(2)}°C ={' '}
            {fahrenheit.toFixed(2)}°F
          </p>
        ) : (
          <p>Enter a temperature</p>
        )}
      </div>
    </div>
  );
}

🔥 KEY PATTERNS:

  1. State Lifted to Parent:
jsx
// Parent owns state
const [temperature, setTemperature] = useState('');
const [scale, setScale] = useState('c');
  1. Children Are "Controlled":
jsx
// Child receives value via props (controlled component pattern)
<TemperatureInput
  temperature={temperature} // ✅ Props down
  onTemperatureChange={handleChange} // ✅ Callbacks up
/>
  1. Inverse Data Flow:
jsx
// Child calls callback when user types
<input onChange={(e) => onTemperatureChange(e.target.value)} />;

// Parent updates state
const handleCelsiusChange = (temp) => {
  setTemperature(temp); // ✅ Parent controls state
};
  1. Single Source of Truth:
jsx
// ✅ Chỉ 1 state temperature for nhiệt độ
// Cả hai đầu vào đều lấy giá trị từ state này
const celsius = scale === 'f' ? toCelsius(temperature) : temperature;
const fahrenheit = scale === 'c' ? toFahrenheit(temperature) : temperature;

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

jsx
// ✅ Product Card (presentational - no state)
function ProductCard({ product, onAddToCart }) {
  return (
    <div
      style={{
        border: '1px solid #ddd',
        padding: '15px',
        borderRadius: '8px',
        marginBottom: '10px',
      }}
    >
      <h3>{product.name}</h3>
      <p style={{ color: '#666' }}>${product.price}</p>
      <button
        onClick={() => onAddToCart(product)}
        style={{
          padding: '8px 16px',
          background: '#007bff',
          color: 'white',
          border: 'none',
          borderRadius: '4px',
          cursor: 'pointer',
        }}
      >
        Add to Cart
      </button>
    </div>
  );
}

// ✅ Product List (receives data + callbacks)
function ProductList({ products, onAddToCart }) {
  return (
    <div style={{ flex: 1, marginRight: '20px' }}>
      <h2>📦 Products</h2>
      {products.map((product) => (
        <ProductCard
          key={product.id}
          product={product}
          onAddToCart={onAddToCart}
        />
      ))}
    </div>
  );
}

// ✅ Cart Summary (receives cart data + callbacks)
function CartSummary({ cartItems, onRemoveFromCart, onClearCart }) {
  const total = cartItems.reduce(
    (sum, item) => sum + item.price * item.quantity,
    0,
  );

  return (
    <div
      style={{
        flex: 1,
        background: '#f8f9fa',
        padding: '20px',
        borderRadius: '8px',
        height: 'fit-content',
      }}
    >
      <h2>🛒 Cart ({cartItems.length})</h2>

      {cartItems.length === 0 ? (
        <p style={{ color: '#666' }}>Cart is empty</p>
      ) : (
        <>
          {cartItems.map((item) => (
            <div
              key={item.id}
              style={{
                marginBottom: '10px',
                paddingBottom: '10px',
                borderBottom: '1px solid #ddd',
              }}
            >
              <div
                style={{
                  display: 'flex',
                  justifyContent: 'space-between',
                  alignItems: 'center',
                }}
              >
                <div>
                  <strong>{item.name}</strong>
                  <p style={{ margin: '5px 0', color: '#666' }}>
                    ${item.price} × {item.quantity} = $
                    {item.price * item.quantity}
                  </p>
                </div>
                <button
                  onClick={() => onRemoveFromCart(item.id)}
                  style={{
                    padding: '5px 10px',
                    background: '#dc3545',
                    color: 'white',
                    border: 'none',
                    borderRadius: '4px',
                    cursor: 'pointer',
                  }}
                >
                  Remove
                </button>
              </div>
            </div>
          ))}

          <div
            style={{
              marginTop: '20px',
              paddingTop: '20px',
              borderTop: '2px solid #333',
            }}
          >
            <h3>Total: ${total.toFixed(2)}</h3>
            <button
              onClick={onClearCart}
              style={{
                width: '100%',
                padding: '10px',
                background: '#6c757d',
                color: 'white',
                border: 'none',
                borderRadius: '4px',
                cursor: 'pointer',
                marginTop: '10px',
              }}
            >
              Clear Cart
            </button>
          </div>
        </>
      )}
    </div>
  );
}

// ✅ Parent Component - Owns State
function ShoppingApp() {
  // ✅ State lifted to parent (shared by ProductList and CartSummary)
  const [cart, setCart] = useState([]);

  // Sample products
  const products = [
    { id: 1, name: 'Laptop', price: 999 },
    { id: 2, name: 'Mouse', price: 29 },
    { id: 3, name: 'Keyboard', price: 79 },
    { id: 4, name: 'Monitor', price: 299 },
  ];

  // ✅ Handler: Add to cart
  const handleAddToCart = (product) => {
    setCart((prev) => {
      // Check if product already in cart
      const existingItem = prev.find((item) => item.id === product.id);

      if (existingItem) {
        // Increase quantity
        return prev.map((item) =>
          item.id === product.id
            ? { ...item, quantity: item.quantity + 1 }
            : item,
        );
      } else {
        // Add new item
        return [...prev, { ...product, quantity: 1 }];
      }
    });
  };

  // ✅ Handler: Remove from cart
  const handleRemoveFromCart = (productId) => {
    setCart((prev) => prev.filter((item) => item.id !== productId));
  };

  // ✅ Handler: Clear cart
  const handleClearCart = () => {
    setCart([]);
  };

  return (
    <div style={{ padding: '20px', fontFamily: 'system-ui' }}>
      <h1>🛍️ Shopping Cart App</h1>

      <div style={{ display: 'flex', gap: '20px', marginTop: '20px' }}>
        {/* ✅ Pass state + callbacks down */}
        <ProductList
          products={products}
          onAddToCart={handleAddToCart}
        />

        <CartSummary
          cartItems={cart}
          onRemoveFromCart={handleRemoveFromCart}
          onClearCart={handleClearCart}
        />
      </div>
    </div>
  );
}

🔥 KEY LEARNINGS:

  1. State Placement:
jsx
// ✅ State in parent (ShoppingApp)
const [cart, setCart] = useState([]);

// ❌ WRONG: State in ProductList
// CartSummary wouldn't be able to access it!
  1. Data Flow:
ShoppingApp (state owner)
    ↓                        ↓
ProductList              CartSummary
(receives callbacks)     (receives cart data)

ProductCard
(calls callback)

User clicks "Add to Cart"

Callback → Parent updates state

Cart state changes

CartSummary re-renders with new cart
  1. Props Down, Callbacks Up:
jsx
// ✅ Data flows down via props
<CartSummary cartItems={cart} />

// ✅ Events flow up via callbacks
<ProductList onAddToCart={handleAddToCart} />

Demo 3: Filter + List Pattern - Edge Cases ⭐⭐⭐

jsx
// ✅ Search Bar Component
function SearchBar({ searchTerm, onSearchChange }) {
  return (
    <div style={{ marginBottom: '20px' }}>
      <input
        type='text'
        placeholder='Search users...'
        value={searchTerm}
        onChange={(e) => onSearchChange(e.target.value)}
        style={{
          width: '100%',
          padding: '10px',
          fontSize: '16px',
          border: '2px solid #ddd',
          borderRadius: '4px',
        }}
      />
    </div>
  );
}

// ✅ Filter Buttons
function FilterButtons({ activeFilter, onFilterChange }) {
  const filters = ['all', 'active', 'inactive'];

  return (
    <div style={{ marginBottom: '20px', display: 'flex', gap: '10px' }}>
      {filters.map((filter) => (
        <button
          key={filter}
          onClick={() => onFilterChange(filter)}
          style={{
            padding: '8px 16px',
            background: activeFilter === filter ? '#007bff' : '#e0e0e0',
            color: activeFilter === filter ? 'white' : '#333',
            border: 'none',
            borderRadius: '4px',
            cursor: 'pointer',
            textTransform: 'capitalize',
          }}
        >
          {filter}
        </button>
      ))}
    </div>
  );
}

// ✅ User List Component
function UserList({ users }) {
  if (users.length === 0) {
    return (
      <p style={{ textAlign: 'center', color: '#666', padding: '40px' }}>
        No users found
      </p>
    );
  }

  return (
    <div>
      {users.map((user) => (
        <div
          key={user.id}
          style={{
            padding: '15px',
            marginBottom: '10px',
            border: '1px solid #ddd',
            borderRadius: '4px',
            display: 'flex',
            justifyContent: 'space-between',
            alignItems: 'center',
          }}
        >
          <div>
            <h3 style={{ margin: '0 0 5px 0' }}>{user.name}</h3>
            <p style={{ margin: 0, color: '#666' }}>{user.email}</p>
          </div>
          <span
            style={{
              padding: '4px 12px',
              background: user.isActive ? '#28a745' : '#6c757d',
              color: 'white',
              borderRadius: '12px',
              fontSize: '0.85em',
            }}
          >
            {user.isActive ? 'Active' : 'Inactive'}
          </span>
        </div>
      ))}
    </div>
  );
}

// ✅ Parent Component - Orchestrates Everything
function UserManagement() {
  // ✅ All filter state lifted here
  const [searchTerm, setSearchTerm] = useState('');
  const [activeFilter, setActiveFilter] = useState('all');

  // Sample data
  const allUsers = [
    {
      id: 1,
      name: 'Alice Johnson',
      email: 'alice@example.com',
      isActive: true,
    },
    { id: 2, name: 'Bob Smith', email: 'bob@example.com', isActive: false },
    {
      id: 3,
      name: 'Charlie Brown',
      email: 'charlie@example.com',
      isActive: true,
    },
    { id: 4, name: 'David Lee', email: 'david@example.com', isActive: false },
    { id: 5, name: 'Eve Davis', email: 'eve@example.com', isActive: true },
  ];

  // ✅ Derived state: filtered users
  const filteredUsers = allUsers.filter((user) => {
    // Filter by search term
    const matchesSearch =
      user.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
      user.email.toLowerCase().includes(searchTerm.toLowerCase());

    // Filter by active status
    const matchesFilter =
      activeFilter === 'all' ||
      (activeFilter === 'active' && user.isActive) ||
      (activeFilter === 'inactive' && !user.isActive);

    return matchesSearch && matchesFilter;
  });

  return (
    <div
      style={{
        maxWidth: '800px',
        margin: '0 auto',
        padding: '20px',
        fontFamily: 'system-ui',
      }}
    >
      <h1>👥 User Management</h1>

      {/* ✅ All components controlled by parent */}
      <SearchBar
        searchTerm={searchTerm}
        onSearchChange={setSearchTerm}
      />

      <FilterButtons
        activeFilter={activeFilter}
        onFilterChange={setActiveFilter}
      />

      <div style={{ marginBottom: '10px', color: '#666' }}>
        Showing {filteredUsers.length} of {allUsers.length} users
      </div>

      <UserList users={filteredUsers} />

      {/* Debug info */}
      <details style={{ marginTop: '20px' }}>
        <summary>🔍 Debug: Filter State</summary>
        <pre
          style={{
            background: '#f5f5f5',
            padding: '10px',
            borderRadius: '4px',
          }}
        >
          {JSON.stringify(
            { searchTerm, activeFilter, resultCount: filteredUsers.length },
            null,
            2,
          )}
        </pre>
      </details>
    </div>
  );
}

🔥 Advanced Patterns:

  1. Multiple Filter States:
jsx
// ✅ Parent manages multiple filter criteria
const [searchTerm, setSearchTerm] = useState('');
const [activeFilter, setActiveFilter] = useState('all');
// Could add more: sortBy, dateRange, etc.
  1. Derived Data (Don't Store Filtered List!):
jsx
// ✅ GOOD: Compute filtered list
const filteredUsers = allUsers.filter((user) => {
  return matchesSearch && matchesFilter;
});

// ❌ BAD: Store filtered list in state
// const [filteredUsers, setFilteredUsers] = useState([]);
// This is derived state anti-pattern!
  1. Presentational Components:
jsx
// ✅ Components are "dumb" - just display what they're given
function UserList({ users }) {
  // No state, no logic, just render
  return users.map((user) => <UserCard user={user} />);
}

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

⭐ Exercise 1: Parent-Child Communication (15 phút)

jsx
/**
 * 🎯 Mục tiêu: Implement inverse data flow
 * ⏱️ Thời gian: 15 phút
 * 🚫 KHÔNG dùng: useEffect, useRef, Context
 *
 * Requirements:
 * 1. Counter component với buttons +/-
 * 2. Parent cần biết count value để display
 * 3. Parent có button "Reset" để set count về 0
 * 4. Counter component nhận initial value từ parent
 *
 * 💡 Gợi ý:
 * - State ở đâu? (Parent hay Child?)
 * - Counter là controlled component
 */

// ❌ Starter code (cần sửa):
function Counter() {
  // TODO: Should this have state?

  return (
    <div>
      <button>-</button>
      <span>Count: ???</span>
      <button>+</button>
    </div>
  );
}

function App() {
  // TODO: Add state here?

  return (
    <div>
      <h2>Parent knows count: ???</h2>
      <Counter />
      <button>Reset to 0</button>
    </div>
  );
}

// ✅ NHIỆM VỤ CỦA BẠN:
// TODO: Lift state to parent
// TODO: Make Counter controlled
// TODO: Implement reset functionality
💡 Solution
jsx
// ✅ Counter is controlled component (no internal state)
function Counter({ count, onIncrement, onDecrement }) {
  return (
    <div
      style={{
        display: 'flex',
        gap: '10px',
        alignItems: 'center',
        padding: '20px',
        background: '#f0f0f0',
        borderRadius: '8px',
        width: 'fit-content',
      }}
    >
      <button
        onClick={onDecrement}
        style={{
          padding: '10px 20px',
          fontSize: '20px',
          cursor: 'pointer',
          background: '#dc3545',
          color: 'white',
          border: 'none',
          borderRadius: '4px',
        }}
      >
        -
      </button>
      <span
        style={{
          fontSize: '24px',
          fontWeight: 'bold',
          minWidth: '50px',
          textAlign: 'center',
        }}
      >
        {count}
      </span>
      <button
        onClick={onIncrement}
        style={{
          padding: '10px 20px',
          fontSize: '20px',
          cursor: 'pointer',
          background: '#28a745',
          color: 'white',
          border: 'none',
          borderRadius: '4px',
        }}
      >
        +
      </button>
    </div>
  );
}

// ✅ Parent owns state
function App() {
  const [count, setCount] = useState(0);

  const handleIncrement = () => {
    setCount((prev) => prev + 1);
  };

  const handleDecrement = () => {
    setCount((prev) => prev - 1);
  };

  const handleReset = () => {
    setCount(0);
  };

  return (
    <div style={{ padding: '20px', fontFamily: 'system-ui' }}>
      <h2>Parent knows count: {count}</h2>

      <Counter
        count={count}
        onIncrement={handleIncrement}
        onDecrement={handleDecrement}
      />

      <button
        onClick={handleReset}
        style={{
          marginTop: '20px',
          padding: '10px 20px',
          background: '#6c757d',
          color: 'white',
          border: 'none',
          borderRadius: '4px',
          cursor: 'pointer',
        }}
      >
        Reset to 0
      </button>

      {/* Extra: Show if count is even/odd */}
      <p style={{ marginTop: '20px', color: '#666' }}>
        Count is {count % 2 === 0 ? 'even' : 'odd'}
      </p>
    </div>
  );
}

Key Learnings:

  1. State lifted to parent (App owns count)
  2. Counter is controlled via props
  3. Callbacks flow up (onIncrement, onDecrement)
  4. Parent can control counter (reset button)

⭐⭐ Exercise 2: Todo List with Filter (25 phút)

jsx
/**
 * 🎯 Mục tiêu: Share state giữa multiple siblings
 * ⏱️ Thời gian: 25 phút
 *
 * Scenario: Todo app với separate components
 *
 * Components:
 * - AddTodoForm: Input + button để add todo
 * - FilterButtons: All / Active / Completed
 * - TodoList: Display filtered todos
 * - TodoStats: Show counts (total, active, completed)
 *
 * 🤔 QUESTIONS:
 * 1. State nên ở đâu?
 * 2. Component nào cần callbacks?
 * 3. Derived state gì?
 */

// TODO: Implement these components

function AddTodoForm({ onAddTodo }) {
  // TODO: Local state cho input
  // TODO: Call onAddTodo callback
  return <div>Add Todo Form</div>;
}

function FilterButtons({ activeFilter, onFilterChange }) {
  // TODO: Render filter buttons
  return <div>Filter Buttons</div>;
}

function TodoList({ todos, onToggleTodo, onDeleteTodo }) {
  // TODO: Render todo items
  return <div>Todo List</div>;
}

function TodoStats({ total, active, completed }) {
  // TODO: Display statistics
  return <div>Stats</div>;
}

function TodoApp() {
  // TODO: Design state structure
  // - todos array
  // - filter ('all', 'active', 'completed')

  // TODO: Implement handlers

  // TODO: Compute filtered todos (derived state)

  // TODO: Compute stats (derived state)

  return (
    <div>
      <h1>Todo App</h1>
      {/* TODO: Compose components */}
    </div>
  );
}
💡 Solution
jsx
// ✅ AddTodoForm - has local state for input
function AddTodoForm({ onAddTodo }) {
  const [input, setInput] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    if (input.trim()) {
      onAddTodo(input.trim());
      setInput(''); // Clear input
    }
  };

  return (
    <form
      onSubmit={handleSubmit}
      style={{ marginBottom: '20px' }}
    >
      <input
        type='text'
        value={input}
        onChange={(e) => setInput(e.target.value)}
        placeholder='What needs to be done?'
        style={{ padding: '10px', width: '300px', fontSize: '16px' }}
      />
      <button
        type='submit'
        style={{ padding: '10px 20px', marginLeft: '10px', cursor: 'pointer' }}
      >
        Add
      </button>
    </form>
  );
}

// ✅ FilterButtons
function FilterButtons({ activeFilter, onFilterChange }) {
  const filters = ['all', 'active', 'completed'];

  return (
    <div style={{ marginBottom: '20px' }}>
      {filters.map((filter) => (
        <button
          key={filter}
          onClick={() => onFilterChange(filter)}
          style={{
            padding: '8px 16px',
            marginRight: '10px',
            background: activeFilter === filter ? '#007bff' : '#e0e0e0',
            color: activeFilter === filter ? 'white' : '#333',
            border: 'none',
            borderRadius: '4px',
            cursor: 'pointer',
            textTransform: 'capitalize',
          }}
        >
          {filter}
        </button>
      ))}
    </div>
  );
}

// ✅ TodoList
function TodoList({ todos, onToggleTodo, onDeleteTodo }) {
  if (todos.length === 0) {
    return <p style={{ color: '#666' }}>No todos to show</p>;
  }

  return (
    <div>
      {todos.map((todo) => (
        <div
          key={todo.id}
          style={{
            display: 'flex',
            alignItems: 'center',
            gap: '10px',
            padding: '10px',
            marginBottom: '8px',
            background: '#f8f9fa',
            borderRadius: '4px',
          }}
        >
          <input
            type='checkbox'
            checked={todo.completed}
            onChange={() => onToggleTodo(todo.id)}
          />
          <span
            style={{
              flex: 1,
              textDecoration: todo.completed ? 'line-through' : 'none',
              color: todo.completed ? '#999' : '#000',
            }}
          >
            {todo.text}
          </span>
          <button
            onClick={() => onDeleteTodo(todo.id)}
            style={{
              padding: '5px 10px',
              background: '#dc3545',
              color: 'white',
              border: 'none',
              borderRadius: '4px',
              cursor: 'pointer',
            }}
          >
            Delete
          </button>
        </div>
      ))}
    </div>
  );
}

// ✅ TodoStats
function TodoStats({ total, active, completed }) {
  return (
    <div
      style={{
        marginTop: '20px',
        padding: '15px',
        background: '#e3f2fd',
        borderRadius: '4px',
      }}
    >
      <h3>Statistics</h3>
      <p>Total: {total}</p>
      <p>Active: {active}</p>
      <p>Completed: {completed}</p>
    </div>
  );
}

// ✅ TodoApp - Parent that owns all state
function TodoApp() {
  const [todos, setTodos] = useState([
    { id: 1, text: 'Learn React', completed: false },
    { id: 2, text: 'Build a project', completed: false },
  ]);

  const [filter, setFilter] = useState('all');

  // ✅ Handler: Add todo
  const handleAddTodo = (text) => {
    const newTodo = {
      id: Date.now(),
      text,
      completed: false,
    };
    setTodos((prev) => [...prev, newTodo]);
  };

  // ✅ Handler: Toggle todo
  const handleToggleTodo = (id) => {
    setTodos((prev) =>
      prev.map((todo) =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo,
      ),
    );
  };

  // ✅ Handler: Delete todo
  const handleDeleteTodo = (id) => {
    setTodos((prev) => prev.filter((todo) => todo.id !== id));
  };

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

  // ✅ Derived: Stats
  const stats = {
    total: todos.length,
    active: todos.filter((t) => !t.completed).length,
    completed: todos.filter((t) => t.completed).length,
  };

  return (
    <div
      style={{
        maxWidth: '600px',
        margin: '0 auto',
        padding: '20px',
        fontFamily: 'system-ui',
      }}
    >
      <h1>📝 Todo App</h1>

      <AddTodoForm onAddTodo={handleAddTodo} />

      <FilterButtons
        activeFilter={filter}
        onFilterChange={setFilter}
      />

      <TodoList
        todos={filteredTodos}
        onToggleTodo={handleToggleTodo}
        onDeleteTodo={handleDeleteTodo}
      />

      <TodoStats {...stats} />
    </div>
  );
}

Architecture Decisions:

  1. State in Parent: todos + filter
  2. Local State in Child: AddTodoForm input (doesn't need sharing)
  3. Derived State: filteredTodos, stats (computed, not stored)
  4. 4 Siblings: All controlled by parent via props/callbacks

⭐⭐⭐ Exercise 3: Multi-Select List with Actions (40 phút)

jsx
/**
 * 🎯 Mục tiêu: Complex state sharing với selection
 * ⏱️ Thời gian: 40 phút
 *
 * 📋 Product Requirements:
 * User Story: "Là admin, tôi muốn select nhiều users để thực hiện bulk actions"
 *
 * ✅ Acceptance Criteria:
 * - [ ] User list với checkboxes
 * - [ ] "Select All" checkbox
 * - [ ] Selection count hiển thị
 * - [ ] Bulk actions: Delete Selected, Mark as Active/Inactive
 * - [ ] Actions chỉ available khi có selection
 *
 * Components:
 * - UserListItem: Single user với checkbox
 * - UserList: List of users
 * - SelectionControls: Select All + count
 * - BulkActions: Action buttons
 * - UserManagementApp: Parent orchestrator
 *
 * 🎨 Technical Constraints:
 * - selectedIds tracked in parent (array of IDs)
 * - Users data in parent
 * - All mutations through parent handlers
 *
 * 🚨 Edge Cases:
 * - Select all / deselect all
 * - Delete selected removes from selection
 * - Individual checkbox toggle
 */

// Sample data
const INITIAL_USERS = [
  { id: 1, name: 'Alice', email: 'alice@example.com', isActive: true },
  { id: 2, name: 'Bob', email: 'bob@example.com', isActive: false },
  { id: 3, name: 'Charlie', email: 'charlie@example.com', isActive: true },
  { id: 4, name: 'David', email: 'david@example.com', isActive: false },
];

// ✅ NHIỆM VỤ CỦA BẠN:

function UserListItem({ user, isSelected, onToggleSelect }) {
  // TODO: Render user với checkbox
  return <div>User Item</div>;
}

function UserList({ users, selectedIds, onToggleSelect }) {
  // TODO: Render list of UserListItem
  return <div>User List</div>;
}

function SelectionControls({
  totalUsers,
  selectedCount,
  onSelectAll,
  onDeselectAll,
}) {
  // TODO: Select All checkbox + count display
  return <div>Selection Controls</div>;
}

function BulkActions({
  selectedCount,
  onDelete,
  onMarkActive,
  onMarkInactive,
}) {
  // TODO: Action buttons (disabled if no selection)
  return <div>Bulk Actions</div>;
}

function UserManagementApp() {
  // TODO: State design
  // - users array
  // - selectedIds array

  // TODO: Handlers
  // - handleToggleSelect(id)
  // - handleSelectAll()
  // - handleDeselectAll()
  // - handleDeleteSelected()
  // - handleMarkActive()
  // - handleMarkInactive()

  return (
    <div>
      <h1>User Management</h1>
      {/* TODO: Compose components */}
    </div>
  );
}
💡 Full Solution
jsx
const INITIAL_USERS = [
  { id: 1, name: 'Alice Johnson', email: 'alice@example.com', isActive: true },
  { id: 2, name: 'Bob Smith', email: 'bob@example.com', isActive: false },
  {
    id: 3,
    name: 'Charlie Brown',
    email: 'charlie@example.com',
    isActive: true,
  },
  { id: 4, name: 'David Lee', email: 'david@example.com', isActive: false },
  { id: 5, name: 'Eve Davis', email: 'eve@example.com', isActive: true },
];

// ✅ UserListItem
function UserListItem({ user, isSelected, onToggleSelect }) {
  return (
    <div
      style={{
        display: 'flex',
        alignItems: 'center',
        padding: '12px',
        marginBottom: '8px',
        background: isSelected ? '#e3f2fd' : 'white',
        border: `2px solid ${isSelected ? '#007bff' : '#ddd'}`,
        borderRadius: '4px',
      }}
    >
      <input
        type='checkbox'
        checked={isSelected}
        onChange={() => onToggleSelect(user.id)}
        style={{
          marginRight: '15px',
          width: '18px',
          height: '18px',
          cursor: 'pointer',
        }}
      />

      <div style={{ flex: 1 }}>
        <h4 style={{ margin: '0 0 5px 0' }}>{user.name}</h4>
        <p style={{ margin: 0, color: '#666', fontSize: '0.9em' }}>
          {user.email}
        </p>
      </div>

      <span
        style={{
          padding: '4px 12px',
          background: user.isActive ? '#28a745' : '#6c757d',
          color: 'white',
          borderRadius: '12px',
          fontSize: '0.85em',
        }}
      >
        {user.isActive ? 'Active' : 'Inactive'}
      </span>
    </div>
  );
}

// ✅ UserList
function UserList({ users, selectedIds, onToggleSelect }) {
  return (
    <div style={{ marginBottom: '20px' }}>
      {users.map((user) => (
        <UserListItem
          key={user.id}
          user={user}
          isSelected={selectedIds.includes(user.id)}
          onToggleSelect={onToggleSelect}
        />
      ))}
    </div>
  );
}

// ✅ SelectionControls
function SelectionControls({
  totalUsers,
  selectedCount,
  onSelectAll,
  onDeselectAll,
}) {
  const allSelected = selectedCount === totalUsers && totalUsers > 0;

  return (
    <div
      style={{
        marginBottom: '20px',
        padding: '15px',
        background: '#f8f9fa',
        borderRadius: '4px',
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'space-between',
      }}
    >
      <div style={{ display: 'flex', alignItems: 'center', gap: '15px' }}>
        <label
          style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}
        >
          <input
            type='checkbox'
            checked={allSelected}
            onChange={allSelected ? onDeselectAll : onSelectAll}
            style={{
              marginRight: '8px',
              width: '18px',
              height: '18px',
              cursor: 'pointer',
            }}
          />
          <span style={{ fontWeight: 'bold' }}>Select All</span>
        </label>

        <span style={{ color: '#666' }}>
          {selectedCount} of {totalUsers} selected
        </span>
      </div>

      {selectedCount > 0 && (
        <button
          onClick={onDeselectAll}
          style={{
            padding: '6px 12px',
            background: '#6c757d',
            color: 'white',
            border: 'none',
            borderRadius: '4px',
            cursor: 'pointer',
          }}
        >
          Clear Selection
        </button>
      )}
    </div>
  );
}

// ✅ BulkActions
function BulkActions({
  selectedCount,
  onDelete,
  onMarkActive,
  onMarkInactive,
}) {
  const disabled = selectedCount === 0;

  const buttonStyle = (color) => ({
    padding: '10px 20px',
    background: disabled ? '#ccc' : color,
    color: 'white',
    border: 'none',
    borderRadius: '4px',
    cursor: disabled ? 'not-allowed' : 'pointer',
    marginRight: '10px',
    opacity: disabled ? 0.6 : 1,
  });

  return (
    <div style={{ marginBottom: '20px' }}>
      <h3>Bulk Actions {selectedCount > 0 && `(${selectedCount} selected)`}</h3>
      <div>
        <button
          onClick={onDelete}
          disabled={disabled}
          style={buttonStyle('#dc3545')}
        >
          🗑️ Delete Selected
        </button>

        <button
          onClick={onMarkActive}
          disabled={disabled}
          style={buttonStyle('#28a745')}
        >
          ✅ Mark as Active
        </button>

        <button
          onClick={onMarkInactive}
          disabled={disabled}
          style={buttonStyle('#6c757d')}
        >
          ❌ Mark as Inactive
        </button>
      </div>
    </div>
  );
}

// ✅ UserManagementApp - Parent orchestrator
function UserManagementApp() {
  const [users, setUsers] = useState(INITIAL_USERS);
  const [selectedIds, setSelectedIds] = useState([]);

  // ✅ Toggle single selection
  const handleToggleSelect = (id) => {
    setSelectedIds((prev) => {
      if (prev.includes(id)) {
        return prev.filter((selectedId) => selectedId !== id);
      } else {
        return [...prev, id];
      }
    });
  };

  // ✅ Select all
  const handleSelectAll = () => {
    setSelectedIds(users.map((user) => user.id));
  };

  // ✅ Deselect all
  const handleDeselectAll = () => {
    setSelectedIds([]);
  };

  // ✅ Delete selected
  const handleDeleteSelected = () => {
    if (window.confirm(`Delete ${selectedIds.length} users?`)) {
      setUsers((prev) => prev.filter((user) => !selectedIds.includes(user.id)));
      setSelectedIds([]);
    }
  };

  // ✅ Mark selected as active
  const handleMarkActive = () => {
    setUsers((prev) =>
      prev.map((user) =>
        selectedIds.includes(user.id) ? { ...user, isActive: true } : user,
      ),
    );
    setSelectedIds([]);
  };

  // ✅ Mark selected as inactive
  const handleMarkInactive = () => {
    setUsers((prev) =>
      prev.map((user) =>
        selectedIds.includes(user.id) ? { ...user, isActive: false } : user,
      ),
    );
    setSelectedIds([]);
  };

  return (
    <div
      style={{
        maxWidth: '800px',
        margin: '0 auto',
        padding: '20px',
        fontFamily: 'system-ui',
      }}
    >
      <h1>👥 User Management</h1>

      <SelectionControls
        totalUsers={users.length}
        selectedCount={selectedIds.length}
        onSelectAll={handleSelectAll}
        onDeselectAll={handleDeselectAll}
      />

      <BulkActions
        selectedCount={selectedIds.length}
        onDelete={handleDeleteSelected}
        onMarkActive={handleMarkActive}
        onMarkInactive={handleMarkInactive}
      />

      <UserList
        users={users}
        selectedIds={selectedIds}
        onToggleSelect={handleToggleSelect}
      />

      {users.length === 0 && (
        <p style={{ textAlign: 'center', color: '#666', padding: '40px' }}>
          No users available
        </p>
      )}

      {/* Debug */}
      <details style={{ marginTop: '30px' }}>
        <summary>🔍 Debug Info</summary>
        <div style={{ fontSize: '0.9em' }}>
          <p>
            <strong>Total Users:</strong> {users.length}
          </p>
          <p>
            <strong>Selected IDs:</strong> [{selectedIds.join(', ')}]
          </p>
          <pre
            style={{ background: '#f5f5f5', padding: '10px', overflow: 'auto' }}
          >
            {JSON.stringify(users, null, 2)}
          </pre>
        </div>
      </details>
    </div>
  );
}

Key Patterns:

  1. Selection state lifted to parent: selectedIds array
  2. Toggle logic in parent: Add/remove from selectedIds
  3. Bulk operations: Filter/map users based on selectedIds
  4. Clear selection after action: Good UX
  5. Confirmation for destructive actions: Delete prompt

⭐⭐⭐⭐ Exercise 4: Accordion with Controlled Expansion (60 phút)

jsx
/**
 * 🎯 Mục tiêu: Implement accordion với controlled expansion state
 * ⏱️ Thời gian: 60 phút
 *
 * 🏗️ PHASE 1: Research & Design (20 phút)
 *
 * Requirements:
 * - Accordion với multiple panels
 * - Modes: single (only 1 open) vs multiple (many open)
 * - Expand/collapse animations (CSS transition)
 * - Expand All / Collapse All buttons
 * - Keyboard navigation (Enter to toggle)
 *
 * Design Questions:
 * 1. State structure: array of open IDs vs object?
 * 2. Where to track mode (single vs multiple)?
 * 3. How to prevent multiple open in single mode?
 * 4. How to handle Expand All in single mode?
 *
 * 💻 PHASE 2: Implementation (30 phút)
 * 🧪 PHASE 3: Testing (10 phút)
 */

const FAQ_DATA = [
  {
    id: 1,
    question: 'What is lifting state up?',
    answer:
      'Lifting state up means moving state to the closest common ancestor when multiple components need to share that state.',
  },
  {
    id: 2,
    question: 'When should I lift state?',
    answer:
      'Lift state when two or more sibling components need to access or modify the same data.',
  },
  {
    id: 3,
    question: 'What are the trade-offs?',
    answer:
      'Lifting state can cause more components to re-render, but it enables proper data sharing and maintains a single source of truth.',
  },
];

// Implement AccordionPanel and Accordion components!
💡 Hint & Starter Code
jsx
/**
 * State Design Recommendation:
 * - openIds: array of panel IDs that are currently open
 * - mode: 'single' or 'multiple'
 *
 * Single mode: openIds.length <= 1 (enforce in toggle logic)
 * Multiple mode: openIds can have any length
 */

function AccordionPanel({ id, question, answer, isOpen, onToggle }) {
  // TODO: Implement panel with expand/collapse
}

function Accordion() {
  // TODO: State for openIds and mode
  // TODO: Handlers for toggle, expandAll, collapseAll
  // TODO: Render panels
}
💡 Solution
jsx
/**
 * AccordionPanel - một panel đơn trong accordion
 * @param {Object} props
 * @param {number} props.id - ID duy nhất của panel
 * @param {string} props.question - Tiêu đề câu hỏi
 * @param {string} props.answer - Nội dung trả lời
 * @param {boolean} props.isOpen - Trạng thái mở/đóng
 * @param {Function} props.onToggle - Callback khi click để toggle
 */
function AccordionPanel({ id, question, answer, isOpen, onToggle }) {
  return (
    <div
      style={{
        border: '1px solid #ddd',
        borderRadius: '6px',
        marginBottom: '12px',
        overflow: 'hidden',
      }}
    >
      <button
        onClick={() => onToggle(id)}
        style={{
          width: '100%',
          padding: '16px 20px',
          textAlign: 'left',
          background: isOpen ? '#f0f7ff' : '#f8f9fa',
          border: 'none',
          fontSize: '1.1rem',
          fontWeight: isOpen ? '600' : '500',
          cursor: 'pointer',
          display: 'flex',
          justifyContent: 'space-between',
          alignItems: 'center',
          transition: 'background 0.2s',
        }}
      >
        <span>{question}</span>
        <span
          style={{
            fontSize: '1.4rem',
            transform: isOpen ? 'rotate(180deg)' : 'rotate(0deg)',
            transition: 'transform 0.3s',
          }}
        >

        </span>
      </button>

      <div
        style={{
          maxHeight: isOpen ? '500px' : '0',
          overflow: 'hidden',
          transition: 'max-height 0.4s ease-out, padding 0.4s ease-out',
          padding: isOpen ? '0 20px 20px' : '0 20px',
          background: 'white',
        }}
      >
        <p style={{ margin: '16px 0 0 0', lineHeight: '1.6' }}>{answer}</p>
      </div>
    </div>
  );
}

/**
 * Accordion - component chính điều khiển nhiều panel
 * Hỗ trợ hai chế độ: single (chỉ mở 1) và multiple (mở nhiều cùng lúc)
 */
function Accordion() {
  const [openIds, setOpenIds] = React.useState([]);
  const [mode, setMode] = React.useState('single'); // 'single' hoặc 'multiple'

  const togglePanel = (id) => {
    setOpenIds((prev) => {
      if (prev.includes(id)) {
        // Đóng panel đang mở
        return prev.filter((panelId) => panelId !== id);
      } else {
        // Mở panel
        if (mode === 'single') {
          // Chỉ cho phép mở 1 panel
          return [id];
        }
        // Multiple mode: thêm vào danh sách
        return [...prev, id];
      }
    });
  };

  const expandAll = () => {
    if (mode === 'single') {
      // Trong single mode, expand all sẽ mở panel đầu tiên
      setOpenIds([FAQ_DATA[0]?.id]);
    } else {
      setOpenIds(FAQ_DATA.map((item) => item.id));
    }
  };

  const collapseAll = () => {
    setOpenIds([]);
  };

  const isAllExpanded =
    openIds.length === FAQ_DATA.length && FAQ_DATA.length > 0;

  return (
    <div style={{ maxWidth: '800px', margin: '0 auto', padding: '20px' }}>
      <h1>FAQ Accordion</h1>

      <div
        style={{
          marginBottom: '24px',
          display: 'flex',
          gap: '16px',
          flexWrap: 'wrap',
        }}
      >
        <div>
          <strong>Mode: </strong>
          <select
            value={mode}
            onChange={(e) => {
              setMode(e.target.value);
              // Khi chuyển sang single mode → chỉ giữ lại 1 panel (hoặc đóng hết)
              if (e.target.value === 'single' && openIds.length > 1) {
                setOpenIds(openIds.slice(0, 1));
              }
            }}
            style={{ padding: '6px 10px', fontSize: '1rem' }}
          >
            <option value='single'>Single (chỉ 1 mở)</option>
            <option value='multiple'>Multiple (mở nhiều)</option>
          </select>
        </div>

        <button
          onClick={expandAll}
          style={{ padding: '8px 16px', cursor: 'pointer' }}
        >
          Expand All
        </button>

        <button
          onClick={collapseAll}
          style={{ padding: '8px 16px', cursor: 'pointer' }}
        >
          Collapse All
        </button>

        <span style={{ alignSelf: 'center', color: '#555' }}>
          {openIds.length} / {FAQ_DATA.length} mở
        </span>
      </div>

      {FAQ_DATA.map((item) => (
        <AccordionPanel
          key={item.id}
          id={item.id}
          question={item.question}
          answer={item.answer}
          isOpen={openIds.includes(item.id)}
          onToggle={togglePanel}
        />
      ))}
    </div>
  );
}

// Dữ liệu mẫu (đã có trong đề bài)
const FAQ_DATA = [
  {
    id: 1,
    question: 'What is lifting state up?',
    answer:
      'Lifting state up means moving state to the closest common ancestor when multiple components need to share that state.',
  },
  {
    id: 2,
    question: 'When should I lift state?',
    answer:
      'Lift state when two or more sibling components need to access or modify the same data.',
  },
  {
    id: 3,
    question: 'What are the trade-offs?',
    answer:
      'Lifting state can cause more components to re-render, but it enables proper data sharing and maintains a single source of truth.',
  },
];

// Để chạy thử: <Accordion />

Kết quả ví dụ:

  • Chế độ single: chỉ có tối đa 1 panel mở cùng lúc
  • Chế độ multiple: có thể mở tất cả các panel
  • Nút Expand All / Collapse All hoạt động theo mode hiện tại
  • Có animation mượt khi mở/đóng (dùng max-height + transition)
  • Hiển thị số lượng panel đang mở
  • Chuyển mode tự động điều chỉnh trạng thái hợp lý

⭐⭐⭐⭐⭐ Exercise 5: Kanban Board (90 phút)

jsx
/**
 * 🎯 Mục tiêu: Production-ready Kanban board với drag simulation
 * ⏱️ Thời gian: 90 phút
 *
 * 📋 Feature Specification:
 * - 3 columns: Todo, In Progress, Done
 * - Add task to any column
 * - Move tasks between columns (buttons - no actual drag & drop)
 * - Delete tasks
 * - Task count per column
 * - Filter: show all columns or single column
 *
 * 🏗️ Technical Design:
 * 1. Tasks array với column property
 * 2. Columns config (id, title, color)
 * 3. Move task = update column property
 * 4. All operations through parent
 *
 * Components:
 * - Task: Single task card với move buttons
 * - Column: Task container với add form
 * - Board: Parent orchestrator
 * - Stats: Summary statistics
 *
 * ✅ Production Checklist:
 * - [ ] State structure documented
 * - [ ] Immutable updates
 * - [ ] Proper error handling (empty task text)
 * - [ ] Keyboard shortcuts (optional)
 * - [ ] Responsive layout
 */

const COLUMNS = [
  { id: 'todo', title: 'To Do', color: '#6c757d' },
  { id: 'inprogress', title: 'In Progress', color: '#ffc107' },
  { id: 'done', title: 'Done', color: '#28a745' },
];

// Implement the Kanban board!
💡 Solution
jsx
/**
 * Task - Single task card hiển thị thông tin task và nút di chuyển
 * @param {Object} props
 * @param {Object} props.task - Object task {id, title, description?, column}
 * @param {Function} props.onMove - Callback khi di chuyển task (taskId, newColumn)
 * @param {Function} props.onDelete - Callback khi xóa task (taskId)
 */
function Task({ task, onMove, onDelete }) {
  const columns = [
    { id: 'todo', title: 'To Do' },
    { id: 'inprogress', title: 'In Progress' },
    { id: 'done', title: 'Done' },
  ];

  const otherColumns = columns.filter((c) => c.id !== task.column);

  return (
    <div
      style={{
        background: 'white',
        border: '1px solid #ddd',
        borderRadius: '8px',
        padding: '12px 16px',
        marginBottom: '12px',
        boxShadow: '0 2px 4px rgba(0,0,0,0.08)',
      }}
    >
      <h4 style={{ margin: '0 0 8px 0', fontSize: '1.1rem' }}>{task.title}</h4>

      {task.description && (
        <p style={{ margin: '0 0 12px 0', color: '#555', fontSize: '0.95rem' }}>
          {task.description}
        </p>
      )}

      <div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
        {otherColumns.map((col) => (
          <button
            key={col.id}
            onClick={() => onMove(task.id, col.id)}
            style={{
              padding: '6px 12px',
              fontSize: '0.85rem',
              background: '#f0f0f0',
              border: '1px solid #ccc',
              borderRadius: '4px',
              cursor: 'pointer',
            }}
          >
            → {col.title}
          </button>
        ))}

        <button
          onClick={() => onDelete(task.id)}
          style={{
            padding: '6px 12px',
            fontSize: '0.85rem',
            background: '#ffebee',
            color: '#c62828',
            border: '1px solid #ef9a9a',
            borderRadius: '4px',
            cursor: 'pointer',
            marginLeft: 'auto',
          }}
        >
          Delete
        </button>
      </div>
    </div>
  );
}

/**
 * Column - Một cột trong Kanban board
 * @param {Object} props
 * @param {string} props.id - id của column ('todo' | 'inprogress' | 'done')
 * @param {string} props.title - Tiêu đề hiển thị
 * @param {string} props.color - Màu chủ đạo
 * @param {Array} props.tasks - Danh sách tasks thuộc column này
 * @param {Function} props.onMove - Callback di chuyển task
 * @param {Function} props.onDelete - Callback xóa task
 * @param {Function} props.onAddTask - Callback thêm task mới vào column
 */
function Column({ id, title, color, tasks, onMove, onDelete, onAddTask }) {
  const [newTitle, setNewTitle] = React.useState('');

  const handleAdd = (e) => {
    e.preventDefault();
    if (newTitle.trim()) {
      onAddTask(id, newTitle.trim());
      setNewTitle('');
    }
  };

  return (
    <div
      style={{
        background: '#f8f9fa',
        borderRadius: '8px',
        padding: '16px',
        flex: '1',
        minWidth: '280px',
        display: 'flex',
        flexDirection: 'column',
      }}
    >
      <div
        style={{
          background: color,
          color: 'white',
          padding: '12px 16px',
          borderRadius: '6px',
          marginBottom: '16px',
          fontWeight: 'bold',
          display: 'flex',
          justifyContent: 'space-between',
          alignItems: 'center',
        }}
      >
        <span>{title}</span>
        <span style={{ fontSize: '0.9rem', opacity: 0.9 }}>{tasks.length}</span>
      </div>

      <div style={{ flex: 1, minHeight: '200px' }}>
        {tasks.map((task) => (
          <Task
            key={task.id}
            task={task}
            onMove={onMove}
            onDelete={onDelete}
          />
        ))}
      </div>

      <form
        onSubmit={handleAdd}
        style={{ marginTop: '16px' }}
      >
        <input
          type='text'
          value={newTitle}
          onChange={(e) => setNewTitle(e.target.value)}
          placeholder='Add a task...'
          style={{
            width: '100%',
            padding: '10px',
            border: '1px solid #ddd',
            borderRadius: '6px',
            marginBottom: '8px',
            fontSize: '0.95rem',
          }}
        />
        <button
          type='submit'
          disabled={!newTitle.trim()}
          style={{
            width: '100%',
            padding: '10px',
            background: color,
            color: 'white',
            border: 'none',
            borderRadius: '6px',
            cursor: newTitle.trim() ? 'pointer' : 'not-allowed',
            opacity: newTitle.trim() ? 1 : 0.6,
          }}
        >
          + Add Task
        </button>
      </form>
    </div>
  );
}

/**
 * Kanban Board chính - điều phối toàn bộ state và logic
 */
function KanbanBoard() {
  const COLUMNS = [
    { id: 'todo', title: 'To Do', color: '#6c757d' },
    { id: 'inprogress', title: 'In Progress', color: '#ffc107' },
    { id: 'done', title: 'Done', color: '#28a745' },
  ];

  const [tasks, setTasks] = React.useState([
    { id: 1, title: 'Setup project structure', column: 'todo' },
    { id: 2, title: 'Design database schema', column: 'todo' },
    { id: 3, title: 'Implement authentication', column: 'inprogress' },
    { id: 4, title: 'Create landing page', column: 'done' },
  ]);

  const [filter, setFilter] = React.useState('all'); // 'all' | columnId

  const addTask = (columnId, title) => {
    const newTask = {
      id: Date.now(),
      title,
      column: columnId,
    };
    setTasks((prev) => [...prev, newTask]);
  };

  const moveTask = (taskId, newColumn) => {
    setTasks((prev) =>
      prev.map((task) =>
        task.id === taskId ? { ...task, column: newColumn } : task,
      ),
    );
  };

  const deleteTask = (taskId) => {
    if (window.confirm('Delete this task?')) {
      setTasks((prev) => prev.filter((t) => t.id !== taskId));
    }
  };

  // Nhóm tasks theo column
  const tasksByColumn = COLUMNS.reduce((acc, col) => {
    acc[col.id] = tasks.filter((t) => t.column === col.id);
    return acc;
  }, {});

  // Lọc theo filter
  const visibleColumns =
    filter === 'all' ? COLUMNS : COLUMNS.filter((c) => c.id === filter);

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

      <div
        style={{
          marginBottom: '24px',
          display: 'flex',
          gap: '16px',
          flexWrap: 'wrap',
        }}
      >
        <button
          onClick={() => setFilter('all')}
          style={{
            padding: '8px 16px',
            background: filter === 'all' ? '#007bff' : '#e9ecef',
            color: filter === 'all' ? 'white' : '#333',
            border: 'none',
            borderRadius: '6px',
            cursor: 'pointer',
          }}
        >
          Show All Columns
        </button>

        {COLUMNS.map((col) => (
          <button
            key={col.id}
            onClick={() => setFilter(col.id)}
            style={{
              padding: '8px 16px',
              background: filter === col.id ? col.color : '#e9ecef',
              color: filter === col.id ? 'white' : '#333',
              border: 'none',
              borderRadius: '6px',
              cursor: 'pointer',
            }}
          >
            {col.title} only
          </button>
        ))}
      </div>

      <div
        style={{
          display: 'flex',
          gap: '20px',
          overflowX: 'auto',
          paddingBottom: '16px',
        }}
      >
        {visibleColumns.map((col) => (
          <Column
            key={col.id}
            id={col.id}
            title={col.title}
            color={col.color}
            tasks={tasksByColumn[col.id]}
            onMove={moveTask}
            onDelete={deleteTask}
            onAddTask={addTask}
          />
        ))}
      </div>

      {/* Debug info */}
      <details style={{ marginTop: '40px' }}>
        <summary>Debug: {tasks.length} tasks total</summary>
        <pre
          style={{
            background: '#f5f5f5',
            padding: '12px',
            borderRadius: '6px',
            fontSize: '0.9rem',
          }}
        >
          {JSON.stringify(tasks, null, 2)}
        </pre>
      </details>
    </div>
  );
}

// Để chạy: <KanbanBoard />

Kết quả ví dụ:

  • 3 cột: To Do, In Progress, Done với màu sắc phân biệt
  • Mỗi cột hiển thị số task hiện tại
  • Thêm task mới ngay trong cột (form inline)
  • Di chuyển task giữa các cột bằng nút →
  • Xóa task với confirm dialog
  • Filter xem tất cả hoặc chỉ 1 cột
  • Responsive ngang (scroll ngang trên mobile)
  • State được quản lý tập trung ở KanbanBoard (lifting state up)

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

Bảng So Sánh Trade-offs

PatternƯu điểm ✅Nhược điểm ❌Khi nào dùng 🎯
Lift to Parent• Chia sẻ state giữa các component siblings
• Một nguồn dữ liệu duy nhất
• Dễ debug hơn
• Truyền nhiều props hơn
• Parent re-render khi state thay đổi
• Props drilling
• Các siblings cần chia sẻ dữ liệu
Cách tiếp cận mặc định
Keep State Local• Code đơn giản hơn
• Ít re-render hơn
• Hiệu năng tốt hơn
• Không chia sẻ được
• Có thể bị lặp logic
• State chỉ dùng cho component
• Không cần chia sẻ
• Buffer input form
Callbacks Up, Data Down• Luồng dữ liệu rõ ràng
• Dễ dự đoán
• Dễ lần theo
• Dài dòng khi có nhiều callback
• Props drilling
Pattern React tiêu chuẩn
• Luôn dùng cho việc cập nhật state
Derived State• Luôn đồng bộ
• Không trùng lặp dữ liệu
• Ít bug
• Tính toán lại mỗi lần render
• Có thể tốn kém hiệu năng
• Danh sách đã filter
• Giá trị được tính toán
Ưu tiên hơn việc lưu state
Lift to Grandparent• Chia sẻ cho nhiều component hơn• Props drilling sâu
• Nhiều component trung gian
• Khó bảo trì
• ❌ Tránh nếu có thể
• Dùng Context thay thế (sẽ học sau)

Decision Tree

Q1: Có component nào khác cần dữ liệu này không?
├─ NO → Giữ state local trong component
└─ YES → Tiếp tục Q2

Q2: Các component có cùng parent không?
├─ YES → Lift state lên parent trực tiếp
└─ NO → Tiếp tục Q3

Q3: Có closest common ancestor đủ gần không?
├─ YES → Lift state lên closest common ancestor
└─ NO → Cân nhắc Context API (Ngày 29) hoặc global state (Ngày 10+)

Q4: Dữ liệu có thể derive từ state khác không?
├─ YES → ĐỪNG lưu state! Tính toán nó
└─ NO → Lưu vào state

Q5: Props drilling có quá sâu không (>3 levels)?
├─ YES → Cân nhắc:
│        - Component composition
│        - Context API
│        - Thư viện quản lý state
└─ NO → Props drilling chấp nhận được

NGUYÊN TẮC VÀNG:
✅ State càng gần nơi sử dụng càng tốt
✅ Chỉ lift khi cần chia sẻ
✅ Derived tốt hơn Stored
✅ Dùng callback cho cập nhật, props cho dữ liệu

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

Bug 1: State Not Sharing ⭐

jsx
// 🐛 BUG: ComponentB không nhận được data từ ComponentA
function ComponentA() {
  const [data, setData] = useState('Hello');

  return (
    <div>
      <p>Component A: {data}</p>
      <button onClick={() => setData('Updated!')}>Update</button>
    </div>
  );
}

function ComponentB() {
  return (
    <div>
      <p>Component B: ??? {/* Muốn hiển thị data từ A */}</p>
    </div>
  );
}

function App() {
  return (
    <div>
      <ComponentA />
      <ComponentB />
    </div>
  );
}

/**
 * 🔍 DEBUG QUESTIONS:
 * 1. Tại sao ComponentB không thấy data?
 * 2. State nên di chuyển đến đâu?
 * 3. Fix như thế nào?
 */
💡 Solution

Vấn đề:

  • State trong ComponentA → chỉ ComponentA access được
  • ComponentB là sibling → không thể access state của ComponentA
  • Cần lift state up to parent (App)

Fix:

jsx
// ✅ Lift state to parent
function App() {
  const [data, setData] = useState('Hello');

  return (
    <div>
      <ComponentA
        data={data}
        onUpdate={setData}
      />
      <ComponentB data={data} />
    </div>
  );
}

function ComponentA({ data, onUpdate }) {
  return (
    <div>
      <p>Component A: {data}</p>
      <button onClick={() => onUpdate('Updated!')}>Update</button>
    </div>
  );
}

function ComponentB({ data }) {
  return (
    <div>
      <p>Component B: {data}</p>
    </div>
  );
}

Bài học: Các component siblings không thể chia sẻ state trực tiếp. Hãy lift lên parent chung!


Bug 2: Child Can't Update Parent ⭐⭐

jsx
// 🐛 BUG: Counter update không work
function Counter({ count }) {
  // BUG: Child trying to update parent state directly
  const increment = () => {
    count = count + 1; // ❌ Won't work!
  };

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

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

  return <Counter count={count} />;
}

/**
 * 🔍 DEBUG QUESTIONS:
 * 1. Tại sao button click không update count?
 * 2. `count = count + 1` có vấn đề gì?
 * 3. Child cần gì từ parent?
 */
💡 Solution

Vấn đề:

  1. count là prop (read-only)
  2. Reassigning prop không trigger re-render
  3. Child không thể directly update parent state
  4. Cần callback từ parent!

Fix:

jsx
// ✅ Parent provides callback
function App() {
  const [count, setCount] = useState(0);

  return (
    <Counter
      count={count}
      onIncrement={() => setCount((prev) => prev + 1)} // ✅ Callback!
    />
  );
}

// ✅ Child calls callback
function Counter({ count, onIncrement }) {
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={onIncrement}>+1</button>
    </div>
  );
}

Bài học: Props truyền xuống, callback truyền lên! Child giao tiếp thông qua callback.


Bug 3: Lift Quá Cao ⭐⭐⭐

jsx
// 🐛 VẤN ĐỀ HIỆU NĂNG: Mọi thứ bị re-render không cần thiết
function App() {
  const [searchTerm, setSearchTerm] = useState('');
  const [users, setUsers] = useState([
    /* 1000 users */
  ]);

  const filteredUsers = users.filter((u) =>
    u.name.toLowerCase().includes(searchTerm.toLowerCase()),
  );

  return (
    <div>
      <Header /> {/* Re-render mỗi lần gõ phím! */}
      <Sidebar /> {/* Re-render mỗi lần gõ phím! */}
      <SearchBar
        searchTerm={searchTerm}
        onChange={setSearchTerm}
      />
      <UserList users={filteredUsers} />
      <Footer /> {/* Re-render mỗi lần gõ phím! */}
    </div>
  );
}

🔍 DEBUG QUESTIONS:

    1. Tại sao Header/Sidebar/Footer re-render khi gõ search?
    1. Component nào thực sự cần searchTerm?
    1. Làm thế nào optimize?
💡 Solution

Vấn đề:

  • searchTerm state in App
  • Mỗi keystroke → App re-render → ALL children re-render
  • Header/Sidebar/Footer không cần searchTerm → unnecessary re-renders

Cách sửa 1: Di chuyển state xuống dưới

jsx
// ✅ Tạo component SearchSection
function SearchSection() {
  const [searchTerm, setSearchTerm] = useState('');
  const [users] = useState([
    /* users */
  ]);

  const filteredUsers = users.filter((u) =>
    u.name.toLowerCase().includes(searchTerm.toLowerCase()),
  );

  return (
    <>
      <SearchBar
        searchTerm={searchTerm}
        onChange={setSearchTerm}
      />
      <UserList users={filteredUsers} />
    </>
  );
}

function App() {
  return (
    <div>
      <Header /> {/* ✅ Không bị re-render */}
      <Sidebar /> {/* ✅ Không bị re-render */}
      <SearchSection />
      <Footer /> {/* ✅ Không bị re-render */}
    </div>
  );
}

Cách sửa 2: Dùng React.memo (sẽ học ở Ngày 23)

jsx
// Ngăn re-render không cần thiết
const Header = React.memo(() => <header>Header</header>);

Bài học:

  • Chỉ lift state cao tới mức CẦN THIẾT
  • State càng thấp = ít re-render = hiệu năng tốt hơn
  • Vị trí đặt state ảnh hưởng trực tiếp tới hiệu năng!

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

Knowledge Check

  • [ ] Tôi hiểu khi nào cần lift state up
  • [ ] Tôi biết cách identify closest common ancestor
  • [ ] Tôi có thể implement inverse data flow (callbacks)
  • [ ] Tôi hiểu "props down, callbacks up" pattern
  • [ ] Tôi nhận biết được props drilling
  • [ ] Tôi biết khi nào state nên local vs lifted
  • [ ] Tôi có thể refactor isolated state thành shared state
  • [ ] Tôi hiểu trade-offs của lifting state
  • [ ] Tôi biết cách avoid lifting quá cao
  • [ ] Tôi có thể design component hierarchy với state placement tối ưu

Code Review Checklist

Khi review code về state management:

State Placement:

  • [ ] State ở level thấp nhất có thể
  • [ ] Chỉ lift khi CẦN share
  • [ ] Không lift quá cao (performance)
  • [ ] Local state cho component-specific data

Data Flow:

  • [ ] Data flows down via props
  • [ ] Events flow up via callbacks
  • [ ] Single source of truth
  • [ ] No duplicate state

Component Design:

  • [ ] Parent owns shared state
  • [ ] Children are controlled components
  • [ ] Clear props interface
  • [ ] Derived state computed, not stored

Performance:

  • [ ] Minimal re-renders
  • [ ] State not higher than needed
  • [ ] Consider memoization if many re-renders

🏠 BÀI TẬP VỀ NHÀ

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

Exercise: Refactor Isolated State

Cho code với state isolated. Refactor thành shared state:

jsx
// Start: Each component has its own count
function CounterA() {
  const [count, setCount] = useState(0);
  return (
    <div>
      A: {count} <button onClick={() => setCount((c) => c + 1)}>+</button>
    </div>
  );
}

function CounterB() {
  const [count, setCount] = useState(0);
  return (
    <div>
      B: {count} <button onClick={() => setCount((c) => c + 1)}>+</button>
    </div>
  );
}

// Goal: Share count between A and B, show total
💡 Solution
jsx
/**
 * Counter - Component hiển thị và điều khiển một counter
 * Được thiết kế controlled: không giữ state riêng, nhận giá trị và callbacks từ parent
 * @param {Object} props
 * @param {number} props.count - Giá trị hiện tại của counter
 * @param {Function} props.onIncrement - Callback tăng giá trị
 * @param {Function} props.onDecrement - Callback giảm giá trị
 */
function Counter({ count, onIncrement, onDecrement }) {
  return (
    <div
      style={{
        display: 'flex',
        alignItems: 'center',
        gap: '16px',
        padding: '16px',
        background: '#f8f9fa',
        borderRadius: '8px',
        margin: '12px 0',
      }}
    >
      <button
        onClick={onDecrement}
        style={{
          padding: '10px 18px',
          fontSize: '1.2rem',
          background: '#dc3545',
          color: 'white',
          border: 'none',
          borderRadius: '6px',
          cursor: 'pointer',
        }}
      >
        -
      </button>

      <span
        style={{
          fontSize: '1.8rem',
          fontWeight: 'bold',
          minWidth: '60px',
          textAlign: 'center',
        }}
      >
        {count}
      </span>

      <button
        onClick={onIncrement}
        style={{
          padding: '10px 18px',
          fontSize: '1.2rem',
          background: '#28a745',
          color: 'white',
          border: 'none',
          borderRadius: '6px',
          cursor: 'pointer',
        }}
      >
        +
      </button>
    </div>
  );
}

/**
 * App - Parent component quản lý shared state giữa hai counters
 * Lifting state up để cả hai Counter dùng chung một giá trị count
 */
function App() {
  const [count, setCount] = React.useState(0);

  const handleIncrement = () => {
    setCount((prev) => prev + 1);
  };

  const handleDecrement = () => {
    setCount((prev) => prev - 1);
  };

  return (
    <div style={{ padding: '24px', maxWidth: '600px', margin: '0 auto' }}>
      <h1>Shared Counter Demo</h1>

      <p style={{ fontSize: '1.2rem', marginBottom: '24px' }}>
        Total count: <strong>{count}</strong> (được chia sẻ giữa cả hai counter)
      </p>

      <div>
        <h3>Counter A</h3>
        <Counter
          count={count}
          onIncrement={handleIncrement}
          onDecrement={handleDecrement}
        />
      </div>

      <div>
        <h3>Counter B</h3>
        <Counter
          count={count}
          onIncrement={handleIncrement}
          onDecrement={handleDecrement}
        />
      </div>

      <button
        onClick={() => setCount(0)}
        style={{
          marginTop: '24px',
          padding: '12px 24px',
          fontSize: '1rem',
          background: '#6c757d',
          color: 'white',
          border: 'none',
          borderRadius: '6px',
          cursor: 'pointer',
        }}
      >
        Reset to 0
      </button>
    </div>
  );
}

// Để chạy: <App />

Kết quả ví dụ:

  • Cả Counter A và Counter B đều hiển thị cùng một giá trị count
  • Bấm + hoặc - ở bất kỳ counter nào cũng làm thay đổi giá trị ở cả hai
  • Total count ở trên cùng luôn đồng bộ
  • Nút Reset đưa cả hai counter về 0
  • State được lift lên App → đảm bảo single source of truth, không còn trạng thái riêng lẻ bị lệch nhau

Nâng cao (60 phút)

Exercise: Movie Watchlist Manager

Tạo app quản lý watchlist với:

Features:

  • Movie list với buttons "Add to Watchlist" / "Mark as Watched"
  • Separate tabs: All Movies / Watchlist / Watched
  • Statistics: Total movies, watchlist count, watched count
  • Filter by genre
  • Search by title

Components:

  • MovieCard
  • MovieList
  • Tabs
  • SearchBar
  • GenreFilter
  • Stats
  • MovieApp (parent)

Requirements:

  • ✅ Proper state lifting
  • ✅ All operations through parent
  • ✅ Derived state for filtered lists
  • ✅ No duplicate state
💡 Solution
jsx
/**
 * MovieCard - Hiển thị thông tin một bộ phim với các nút hành động
 * @param {Object} props
 * @param {Object} props.movie - Thông tin phim {id, title, genre, year?}
 * @param {boolean} props.inWatchlist - Có trong watchlist không
 * @param {boolean} props.watched - Đã xem chưa
 * @param {Function} props.onAddToWatchlist - Thêm vào watchlist
 * @param {Function} props.onMarkWatched - Đánh dấu đã xem
 * @param {Function} props.onRemoveFromWatchlist - Xóa khỏi watchlist
 */
function MovieCard({
  movie,
  inWatchlist,
  watched,
  onAddToWatchlist,
  onMarkWatched,
  onRemoveFromWatchlist,
}) {
  return (
    <div
      style={{
        border: '1px solid #ddd',
        borderRadius: '8px',
        padding: '16px',
        marginBottom: '16px',
        background: watched ? '#e8f5e9' : inWatchlist ? '#fff3e0' : 'white',
        transition: 'all 0.2s',
      }}
    >
      <h3 style={{ margin: '0 0 8px 0' }}>{movie.title}</h3>
      <p style={{ margin: '0 0 12px 0', color: '#555' }}>
        {movie.genre} • {movie.year || 'N/A'}
      </p>

      <div style={{ display: 'flex', gap: '12px', flexWrap: 'wrap' }}>
        {!inWatchlist && !watched && (
          <button
            onClick={() => onAddToWatchlist(movie.id)}
            style={{
              padding: '8px 16px',
              background: '#1976d2',
              color: 'white',
              border: 'none',
              borderRadius: '6px',
              cursor: 'pointer',
            }}
          >
            + Watchlist
          </button>
        )}

        {inWatchlist && !watched && (
          <>
            <button
              onClick={() => onMarkWatched(movie.id)}
              style={{
                padding: '8px 16px',
                background: '#388e3c',
                color: 'white',
                border: 'none',
                borderRadius: '6px',
                cursor: 'pointer',
              }}
            >
              ✓ Watched
            </button>

            <button
              onClick={() => onRemoveFromWatchlist(movie.id)}
              style={{
                padding: '8px 16px',
                background: '#d32f2f',
                color: 'white',
                border: 'none',
                borderRadius: '6px',
                cursor: 'pointer',
              }}
            >
              Remove
            </button>
          </>
        )}

        {watched && (
          <button
            onClick={() => onRemoveFromWatchlist(movie.id)}
            style={{
              padding: '8px 16px',
              background: '#757575',
              color: 'white',
              border: 'none',
              borderRadius: '6px',
              cursor: 'pointer',
            }}
          >
            Remove from Watched
          </button>
        )}
      </div>
    </div>
  );
}

/**
 * MovieList - Hiển thị danh sách phim (tất cả / watchlist / watched)
 */
function MovieList({
  movies,
  watchlist,
  watched,
  onAddToWatchlist,
  onMarkWatched,
  onRemoveFromWatchlist,
}) {
  if (movies.length === 0) {
    return (
      <p style={{ color: '#777', textAlign: 'center', padding: '40px 0' }}>
        No movies found
      </p>
    );
  }

  return (
    <div>
      {movies.map((movie) => (
        <MovieCard
          key={movie.id}
          movie={movie}
          inWatchlist={watchlist.includes(movie.id)}
          watched={watched.includes(movie.id)}
          onAddToWatchlist={onAddToWatchlist}
          onMarkWatched={onMarkWatched}
          onRemoveFromWatchlist={onRemoveFromWatchlist}
        />
      ))}
    </div>
  );
}

/**
 * Tabs - Chuyển đổi giữa các view: All / Watchlist / Watched
 */
function Tabs({ activeTab, onTabChange }) {
  const tabs = [
    { id: 'all', label: 'All Movies' },
    { id: 'watchlist', label: 'Watchlist' },
    { id: 'watched', label: 'Watched' },
  ];

  return (
    <div
      style={{
        marginBottom: '24px',
        display: 'flex',
        gap: '8px',
        flexWrap: 'wrap',
      }}
    >
      {tabs.map((tab) => (
        <button
          key={tab.id}
          onClick={() => onTabChange(tab.id)}
          style={{
            padding: '10px 20px',
            background: activeTab === tab.id ? '#1976d2' : '#e0e0e0',
            color: activeTab === tab.id ? 'white' : '#333',
            border: 'none',
            borderRadius: '6px',
            cursor: 'pointer',
            fontWeight: activeTab === tab.id ? 'bold' : 'normal',
          }}
        >
          {tab.label}
        </button>
      ))}
    </div>
  );
}

/**
 * SearchBar + Genre Filter
 */
function Filters({ search, onSearchChange, genre, onGenreChange, genres }) {
  return (
    <div
      style={{
        marginBottom: '24px',
        display: 'flex',
        gap: '16px',
        flexWrap: 'wrap',
      }}
    >
      <input
        type='text'
        value={search}
        onChange={(e) => onSearchChange(e.target.value)}
        placeholder='Search by title...'
        style={{
          flex: 1,
          minWidth: '220px',
          padding: '10px',
          border: '1px solid #ccc',
          borderRadius: '6px',
          fontSize: '1rem',
        }}
      />

      <select
        value={genre}
        onChange={(e) => onGenreChange(e.target.value)}
        style={{
          padding: '10px',
          border: '1px solid #ccc',
          borderRadius: '6px',
          minWidth: '160px',
        }}
      >
        <option value='all'>All Genres</option>
        {genres.map((g) => (
          <option
            key={g}
            value={g}
          >
            {g}
          </option>
        ))}
      </select>
    </div>
  );
}

/**
 * Stats - Thống kê tổng quan
 */
function Stats({ total, inWatchlist, watched }) {
  return (
    <div
      style={{
        background: '#f5f5f5',
        padding: '16px',
        borderRadius: '8px',
        marginBottom: '24px',
        display: 'flex',
        justifyContent: 'space-around',
        flexWrap: 'wrap',
        gap: '16px',
      }}
    >
      <div style={{ textAlign: 'center' }}>
        <strong style={{ fontSize: '1.6rem' }}>{total}</strong>
        <p style={{ margin: '4px 0 0', color: '#555' }}>Total Movies</p>
      </div>
      <div style={{ textAlign: 'center' }}>
        <strong style={{ fontSize: '1.6rem', color: '#1976d2' }}>
          {inWatchlist}
        </strong>
        <p style={{ margin: '4px 0 0', color: '#555' }}>In Watchlist</p>
      </div>
      <div style={{ textAlign: 'center' }}>
        <strong style={{ fontSize: '1.6rem', color: '#388e3c' }}>
          {watched}
        </strong>
        <p style={{ margin: '4px 0 0', color: '#555' }}>Watched</p>
      </div>
    </div>
  );
}

/**
 * MovieWatchlistManager - Component chính quản lý toàn bộ state
 */
function MovieWatchlistManager() {
  const allMovies = [
    { id: 1, title: 'Dune: Part Two', genre: 'Sci-Fi', year: 2024 },
    { id: 2, title: 'Oppenheimer', genre: 'Biography', year: 2023 },
    {
      id: 3,
      title: 'Everything Everywhere All at Once',
      genre: 'Comedy',
      year: 2022,
    },
    { id: 4, title: 'Parasite', genre: 'Thriller', year: 2019 },
    { id: 5, title: 'Inception', genre: 'Sci-Fi', year: 2010 },
    { id: 6, title: 'The Shawshank Redemption', genre: 'Drama', year: 1994 },
    { id: 7, title: 'Interstellar', genre: 'Sci-Fi', year: 2014 },
    { id: 8, title: 'Whiplash', genre: 'Drama', year: 2014 },
  ];

  const [watchlist, setWatchlist] = React.useState([]);
  const [watched, setWatched] = React.useState([]);
  const [tab, setTab] = React.useState('all');
  const [search, setSearch] = React.useState('');
  const [genre, setGenre] = React.useState('all');

  const genres = [...new Set(allMovies.map((m) => m.genre))];

  // Handlers
  const addToWatchlist = (movieId) => {
    if (!watchlist.includes(movieId) && !watched.includes(movieId)) {
      setWatchlist((prev) => [...prev, movieId]);
    }
  };

  const markAsWatched = (movieId) => {
    setWatched((prev) => [...prev, movieId]);
    setWatchlist((prev) => prev.filter((id) => id !== movieId));
  };

  const removeFromList = (movieId) => {
    setWatchlist((prev) => prev.filter((id) => id !== movieId));
    setWatched((prev) => prev.filter((id) => id !== movieId));
  };

  // Filtered & searched movies
  const displayedMovies = allMovies.filter((movie) => {
    const matchesSearch = movie.title
      .toLowerCase()
      .includes(search.toLowerCase());
    const matchesGenre = genre === 'all' || movie.genre === genre;
    return matchesSearch && matchesGenre;
  });

  const visibleMovies =
    tab === 'all'
      ? displayedMovies
      : tab === 'watchlist'
        ? displayedMovies.filter((m) => watchlist.includes(m.id))
        : displayedMovies.filter((m) => watched.includes(m.id));

  const stats = {
    total: allMovies.length,
    inWatchlist: watchlist.length,
    watched: watched.length,
  };

  return (
    <div style={{ maxWidth: '900px', margin: '0 auto', padding: '24px' }}>
      <h1>Movie Watchlist Manager</h1>

      <Stats {...stats} />

      <Filters
        search={search}
        onSearchChange={setSearch}
        genre={genre}
        onGenreChange={setGenre}
        genres={genres}
      />

      <Tabs
        activeTab={tab}
        onTabChange={setTab}
      />

      <MovieList
        movies={visibleMovies}
        watchlist={watchlist}
        watched={watched}
        onAddToWatchlist={addToWatchlist}
        onMarkWatched={markAsWatched}
        onRemoveFromWatchlist={removeFromList}
      />
    </div>
  );
}

Kết quả ví dụ:

  • Tab All Movies: hiển thị toàn bộ danh sách, có thể search + lọc theo thể loại
  • Tab Watchlist: chỉ phim đã thêm vào danh sách muốn xem
  • Tab Watched: chỉ phim đã đánh dấu đã xem
  • Màu nền card thay đổi theo trạng thái (xanh nhạt → đã xem, cam nhạt → trong watchlist)
  • Thống kê realtime: tổng phim, số phim trong watchlist, số phim đã xem
  • State được lift lên component cha → đảm bảo đồng bộ và single source of truth
  • Không có trạng thái trùng lặp, mọi thay đổi đều thông qua parent callbacks

📚 TÀI LIỆU THAM KHẢO

Bắt buộc đọc

  1. React Docs - Sharing State Between Components

  2. React Docs - Passing Data Deeply with Context

Đọc thêm

  1. Thinking in React

  2. Component Composition vs Inheritance


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

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

  • Ngày 13: Forms với State - Controlled components
  • Ngày 12: useState Patterns - Immutability, functional updates
  • Ngày 4: Props - Data flow parent → child

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

  • Ngày 15: Project 2 - Todo App (apply lifting state)
  • Ngày 29: Context API - Solution cho deep props drilling
  • Ngày 30: useReducer - Complex state logic alternative

💡 SENIOR INSIGHTS

Cân Nhắc Production

When to Lift vs Context:

jsx
// ✅ Lift state: 1-2 levels
Parent → Child → Grandchild (OK)

// ⚠️ Props drilling: 3+ levels
Parent → ABCD (Consider Context)

// Rule of Thumb:
// - Lift for shallow trees
// - Context for deep trees
// - State management for global state

Performance Considerations:

jsx
// ⚠️ Expensive re-renders
function App() {
  const [search, setSearch] = useState('');

  return (
    <>
      <ExpensiveComponent /> {/* Re-renders on search change! */}
      <SearchBar
        search={search}
        onChange={setSearch}
      />
    </>
  );
}

// ✅ Optimize với composition
function App() {
  return (
    <>
      <ExpensiveComponent /> {/* Doesn't re-render */}
      <SearchSection /> {/* search state local here */}
    </>
  );
}

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

Junior: Q: "Lifting state up là gì?" A: Di chuyển state từ child lên parent khi nhiều components cần share state đó. Parent pass state down via props và nhận updates qua callbacks.

Mid: Q: "Khi nào nên lift state và khi nào không?" A: Lift khi: (1) Siblings cần share data, (2) Parent cần control child state. Không lift khi: (1) Chỉ 1 component dùng, (2) Component-specific UI state (hover, focus).

Senior: Q: "Design state architecture cho feature X. Justify placement decisions." A: Phải analyze:

  • Component hierarchy (siblings? parent-child?)
  • Data flow requirements (who reads? who writes?)
  • Re-render impact (will lifting cause unnecessary renders?)
  • Scalability (will this grow?)
  • Document: State location + rationale + trade-offs

War Stories

Story 1: The Props Drilling Nightmare

Một project lift state lên App component cho "easier sharing". Kết quả: 8 levels props drilling, mỗi component pass 10+ props. Refactoring nightmare! Lesson: Chỉ lift đến closest common ancestor, không cao hơn. Consider Context nếu > 3 levels.

Story 2: The Performance Bug

Form input lag 200ms mỗi keystroke. Root cause: Form state lifted to App, app có 50 child components không liên quan nhưng re-render mỗi keystroke. Fix: Move form state down to FormSection component. Performance từ 200ms → <1ms. Lesson: State placement = performance!

Story 3: The Duplicate State Sync Issue

Bug: Filtered list và stats không sync. Code store cả filteredData và stats in state, manually sync. Miss 1 chỗ update = out of sync. Fix: Chỉ store raw data + filters, derive filtered list và stats. Luôn sync vì computed. Lesson: Derived state > Duplicate state!


🎯 PREVIEW NGÀY MAI

Ngày 15: Project 2 - Interactive Todo App

Hôm nay đã master state sharing. Ngày mai sẽ áp dụng vào full project:

  • Todo app với multiple components
  • Lift state up trong thực tế
  • Filter, search, stats - all derived
  • Production-ready architecture
  • Review code với senior mindset

Hôm nay: Theory + Patterns ✅
Ngày mai: Real project application 🎯


🎊 CHÚC MỪNG! Bạn đã hoàn thành Ngày 14!

Hôm nay bạn đã master:

  1. ✅ Lifting State Up concept
  2. ✅ Props down, Callbacks up pattern
  3. ✅ Closest common ancestor principle
  4. ✅ State placement decisions
  5. ✅ Avoiding props drilling
  6. ✅ Performance implications

Lifting State Up là foundation của React architecture!

Mọi app phức tạp đều dựa trên principle này. Master nó = master React data flow!

💪 Tomorrow: Put it all together in a real project!

Personal tech knowledge base