Skip to content

📅 NGÀY 47: useTransition - Ưu Tiên Updates Thông Minh

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

  • [ ] Hiểu useTransition hook và khi nào sử dụng
  • [ ] Phân biệt urgent vs non-urgent updates
  • [ ] Implement isPending feedback patterns
  • [ ] Biết trade-offs giữa useTransition và approaches khác
  • [ ] Apply useTransition vào real-world scenarios

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

Câu 1: Trong search interface với 10,000 items, tại sao input bị lag khi user typing?

Câu 2: React 18 concurrent rendering có tự động làm app nhanh hơn không? Tại sao?

Câu 3: Nếu bạn có 2 state updates - một cho input value, một cho filtered results - cái nào urgent hơn?


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

1.1 Vấn Đề Thực Tế

Tưởng tượng bạn build một e-commerce search:

jsx
function ProductSearch() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);

  const handleChange = (e) => {
    const value = e.target.value;
    setQuery(value); // Update input

    // Filter 10,000 products - takes 200ms
    const filtered = products.filter((p) =>
      p.name.toLowerCase().includes(value.toLowerCase()),
    );
    setResults(filtered); // Update results
  };

  return (
    <>
      <input
        value={query}
        onChange={handleChange}
      />
      {results.map((r) => (
        <ProductCard
          key={r.id}
          product={r}
        />
      ))}
    </>
  );
}

Vấn đề: User types "laptop" → UI freezes 200ms mỗi keystroke!

Tại sao? React phải render CẢ HAI updates đồng thời:

  1. Input field (urgent - user cần feedback ngay)
  2. 10,000 product cards (non-urgent - có thể đợi)

Mental Model - Synchronous:

User types "l" → [====== RENDER INPUT + 10,000 CARDS ======] → UI update
                  200ms freeze - Input feels laggy!

1.2 Giải Pháp: useTransition

useTransition cho phép bạn mark một update là "non-urgent" (transition):

jsx
import { useTransition } from 'react';

function ProductSearch() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [isPending, startTransition] = useTransition();

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

    // ✅ URGENT: Update input immediately
    setQuery(value);

    // ✅ NON-URGENT: Defer filtering
    startTransition(() => {
      const filtered = products.filter((p) =>
        p.name.toLowerCase().includes(value.toLowerCase()),
      );
      setResults(filtered);
    });
  };

  return (
    <>
      <input
        value={query}
        onChange={handleChange}
      />
      {isPending && <Spinner />}
      {results.map((r) => (
        <ProductCard
          key={r.id}
          product={r}
        />
      ))}
    </>
  );
}

Mental Model - With Transition:

User types "l" → [== RENDER INPUT ==] → UI update (fast! 16ms)

                 [======== RENDER RESULTS ========] → Background
                  (can be interrupted if user types again)

1.3 Mental Model

Analogy: Restaurant Kitchen

Synchronous (No useTransition):

Customer orders → Chef PHẢI hoàn thành món ăn 100% mới nhận order tiếp
                  (slow service, customers wait)

With useTransition:

Customer orders → Chef lấy order ngay (urgent)

                  Nấu món ăn ở background (non-urgent)

                  Nếu có order mới → Pause nấu, lấy order trước
                  (fast service, responsive)

Key Concepts:

  1. Urgent Updates: User input, clicks, focus - cần instant feedback
  2. Non-urgent Updates: Filtering, sorting, rendering lists - có thể defer
  3. Interruptible: Transition có thể bị interrupt bởi urgent updates
  4. isPending: Flag để show loading state

Visual:

WITHOUT useTransition:
━━━━━━━━━━━━━━━━━━━━━━━━━
Urgent + Non-urgent (blocked)

WITH useTransition:
━━━ Urgent (fast)
    ━━━━━━━━━━━━ Non-urgent (background)
           ↑ Can be interrupted

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

Hiểu lầm 1: "useTransition làm code chạy nhanh hơn"

  • Sự thật: Code vẫn mất thời gian như cũ. useTransition chỉ làm UI responsive hơn bằng cách defer non-urgent work

Hiểu lầm 2: "Nên wrap mọi setState trong startTransition"

  • Sự thật: Chỉ wrap non-urgent updates. Urgent updates (input) KHÔNG nên wrap

Hiểu lầm 3: "isPending luôn true khi startTransition chạy"

  • Sự thật: isPending chỉ true khi transition đang pending. Nếu update nhanh (<16ms), có thể không thấy isPending = true

Hiểu lầm 4: "useTransition tự động optimize performance"

  • Sự thật: Vẫn cần useMemo, React.memo, etc. useTransition chỉ cải thiện perceived performance (UX)

Hiểu lầm 5: "startTransition là async/await"

  • Sự thật: Không phải async! Callback execute synchronously, nhưng React defer rendering

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

Demo 1: Basic useTransition Pattern ⭐

jsx
/**
 * Demo: useTransition cơ bản
 * So sánh behavior với/không có useTransition
 */
import { useState, useTransition } from 'react';

// Helper: Generate heavy data
function generateItems(count) {
  return Array.from({ length: count }, (_, i) => ({
    id: i,
    name: `Item ${i}`,
    value: Math.random(),
  }));
}

// ❌ WITHOUT useTransition
function SearchWithoutTransition() {
  const [query, setQuery] = useState('');
  const [items, setItems] = useState(generateItems(10000));

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

    // Both updates render together
    setQuery(value);

    // Heavy filtering - blocks UI!
    const filtered = generateItems(10000).filter((item) =>
      item.name.includes(value),
    );
    setItems(filtered);
  };

  return (
    <div>
      <h3>❌ Without useTransition</h3>
      <input
        type='text'
        value={query}
        onChange={handleChange}
        placeholder='Type to search (notice lag)...'
      />
      <p>Results: {items.length}</p>
      {items.slice(0, 100).map((item) => (
        <div key={item.id}>{item.name}</div>
      ))}
    </div>
  );
}

// ✅ WITH useTransition
function SearchWithTransition() {
  const [query, setQuery] = useState('');
  const [items, setItems] = useState(generateItems(10000));
  const [isPending, startTransition] = useTransition();

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

    // ✅ URGENT: Update input immediately
    setQuery(value);

    // ✅ NON-URGENT: Defer heavy work
    startTransition(() => {
      const filtered = generateItems(10000).filter((item) =>
        item.name.includes(value),
      );
      setItems(filtered);
    });
  };

  return (
    <div>
      <h3>✅ With useTransition</h3>
      <input
        type='text'
        value={query}
        onChange={handleChange}
        placeholder='Type to search (smooth!)...'
      />
      <p>
        Results: {items.length}
        {isPending && ' (Updating...)'}
      </p>
      {items.slice(0, 100).map((item) => (
        <div
          key={item.id}
          style={{ opacity: isPending ? 0.5 : 1 }}
        >
          {item.name}
        </div>
      ))}
    </div>
  );
}

/**
 * Kết quả:
 * Without: Input lag ~200ms per keystroke
 * With: Input responsive ~16ms, results update in background
 */

Demo 2: Tab Switching với isPending Feedback ⭐⭐

jsx
/**
 * Demo: Tab switching với loading states
 * Real-world pattern: Defer expensive tab content
 */
function TabContainer() {
  const [activeTab, setActiveTab] = useState('home');
  const [isPending, startTransition] = useTransition();

  const handleTabClick = (tab) => {
    // ✅ URGENT: Update active tab immediately (highlight tab)
    // Note: We DON'T wrap this in startTransition
    // because we want instant visual feedback

    // ✅ NON-URGENT: Defer content rendering
    startTransition(() => {
      setActiveTab(tab);
    });
  };

  return (
    <div>
      {/* Tab buttons - instant feedback */}
      <div style={{ display: 'flex', gap: 8, marginBottom: 16 }}>
        {['home', 'profile', 'settings'].map((tab) => (
          <button
            key={tab}
            onClick={() => handleTabClick(tab)}
            style={{
              padding: '8px 16px',
              backgroundColor: activeTab === tab ? '#3b82f6' : '#e5e7eb',
              color: activeTab === tab ? 'white' : 'black',
              border: 'none',
              borderRadius: 4,
              cursor: 'pointer',
              opacity: isPending ? 0.7 : 1,
            }}
          >
            {tab.charAt(0).toUpperCase() + tab.slice(1)}
            {isPending && activeTab === tab && ' ⏳'}
          </button>
        ))}
      </div>

      {/* Tab content - can be deferred */}
      <div
        style={{
          padding: 16,
          border: '1px solid #e5e7eb',
          borderRadius: 4,
          minHeight: 200,
          opacity: isPending ? 0.5 : 1,
          transition: 'opacity 0.2s',
        }}
      >
        {isPending && (
          <div
            style={{
              position: 'absolute',
              top: '50%',
              left: '50%',
              transform: 'translate(-50%, -50%)',
            }}
          >
            Loading...
          </div>
        )}
        <TabContent tab={activeTab} />
      </div>
    </div>
  );
}

// Heavy tab content
function TabContent({ tab }) {
  // Simulate heavy component
  const startTime = performance.now();
  while (performance.now() - startTime < 100) {
    // Busy wait 100ms
  }

  const content = {
    home: 'Welcome to Home! 🏠',
    profile: 'Your Profile 👤',
    settings: 'Settings ⚙️',
  };

  return (
    <div>
      <h2>{content[tab]}</h2>
      <p>This is a heavy component that takes 100ms to render.</p>
      {Array.from({ length: 50 }).map((_, i) => (
        <div key={i}>Content line {i + 1}</div>
      ))}
    </div>
  );
}

/**
 * UX với useTransition:
 * 1. Click tab → Tab highlight ngay lập tức (instant feedback)
 * 2. Old content mờ đi + loading indicator
 * 3. New content render ở background
 * 4. Fade in khi ready
 *
 * Without useTransition:
 * 1. Click tab → Nothing happens
 * 2. Wait 100ms...
 * 3. Tab highlight + content đổi cùng lúc (jarring)
 */

Demo 3: Multiple Transitions với Priority ⭐⭐⭐

jsx
/**
 * Demo: Handle multiple transitions
 * Pattern: Latest transition wins
 */
function AdvancedSearch() {
  const [query, setQuery] = useState('');
  const [category, setCategory] = useState('all');
  const [results, setResults] = useState([]);
  const [isPending, startTransition] = useTransition();

  const allItems = Array.from({ length: 5000 }, (_, i) => ({
    id: i,
    name: `Product ${i}`,
    category: ['electronics', 'clothing', 'food'][i % 3],
  }));

  // Search function
  const performSearch = (searchQuery, searchCategory) => {
    startTransition(() => {
      let filtered = allItems;

      // Filter by query
      if (searchQuery) {
        filtered = filtered.filter((item) =>
          item.name.toLowerCase().includes(searchQuery.toLowerCase()),
        );
      }

      // Filter by category
      if (searchCategory !== 'all') {
        filtered = filtered.filter((item) => item.category === searchCategory);
      }

      setResults(filtered);
    });
  };

  const handleQueryChange = (e) => {
    const value = e.target.value;
    setQuery(value);

    // New transition - interrupts previous one!
    performSearch(value, category);
  };

  const handleCategoryChange = (e) => {
    const value = e.target.value;
    setCategory(value);

    // New transition - interrupts previous one!
    performSearch(query, value);
  };

  return (
    <div>
      <h3>Advanced Search</h3>

      {/* Search input */}
      <input
        type='text'
        value={query}
        onChange={handleQueryChange}
        placeholder='Search products...'
        style={{ padding: 8, marginRight: 8, width: 200 }}
      />

      {/* Category filter */}
      <select
        value={category}
        onChange={handleCategoryChange}
        style={{ padding: 8 }}
      >
        <option value='all'>All Categories</option>
        <option value='electronics'>Electronics</option>
        <option value='clothing'>Clothing</option>
        <option value='food'>Food</option>
      </select>

      {/* Results */}
      <div style={{ marginTop: 16 }}>
        <p>
          Found: {results.length} items
          {isPending && (
            <span style={{ color: '#3b82f6' }}> (Searching...)</span>
          )}
        </p>

        <div style={{ opacity: isPending ? 0.5 : 1 }}>
          {results.slice(0, 50).map((item) => (
            <div
              key={item.id}
              style={{
                padding: 8,
                border: '1px solid #e5e7eb',
                marginBottom: 4,
              }}
            >
              {item.name} - {item.category}
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

/**
 * Key behaviors:
 *
 * 1. Latest transition wins:
 *    Type "a" → Transition starts
 *    Type "ab" → Previous transition interrupted!
 *    Type "abc" → Previous transition interrupted again!
 *    Only last transition completes
 *
 * 2. Multiple triggers share isPending:
 *    isPending true khi BẤT KỲ transition nào pending
 *
 * 3. Stale results prevented:
 *    Không bao giờ show outdated results
 *    React ensures only latest transition commits
 */

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

⭐ Level 1: Basic Transition Implementation (15 phút)

jsx
/**
 * 🎯 Mục tiêu: Implement useTransition cho simple search
 * ⏱️ Thời gian: 15 phút
 * 🚫 KHÔNG dùng: useDeferredValue (chưa học)
 *
 * Requirements:
 * 1. Create search input
 * 2. Filter 1000 items
 * 3. Use useTransition để defer filtering
 * 4. Show isPending state
 *
 * 💡 Gợi ý: setQuery KHÔNG wrap trong startTransition
 */

// ❌ Cách SAI:
function BadTransition() {
  const [query, setQuery] = useState('');
  const [isPending, startTransition] = useTransition();

  const handleChange = (e) => {
    // ❌ Wrapping urgent update in transition!
    startTransition(() => {
      setQuery(e.target.value);
    });
  };

  // Input sẽ lag vì update bị defer!
  return (
    <input
      value={query}
      onChange={handleChange}
    />
  );
}

// ✅ Cách ĐÚNG:
function GoodTransition() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [isPending, startTransition] = useTransition();

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

    // ✅ Urgent: Input update
    setQuery(value);

    // ✅ Non-urgent: Filtering
    startTransition(() => {
      const filtered = filterItems(value);
      setResults(filtered);
    });
  };

  return (
    <>
      <input
        value={query}
        onChange={handleChange}
      />
      {isPending && <div>Loading...</div>}
      {results.map((r) => (
        <div key={r.id}>{r.name}</div>
      ))}
    </>
  );
}

// 🎯 NHIỆM VỤ CỦA BẠN:
function SearchExercise() {
  const items = Array.from({ length: 1000 }, (_, i) => ({
    id: i,
    name: `Item ${i}`,
    description: `Description for item ${i}`,
  }));

  // TODO: Implement state
  // TODO: Implement useTransition
  // TODO: Implement search logic
  // TODO: Show isPending feedback

  return (
    <div>
      {/* TODO: Search input */}
      {/* TODO: Loading indicator */}
      {/* TODO: Results list */}
    </div>
  );
}
💡 Solution
jsx
/**
 * Basic Search với useTransition
 */
function SearchExercise() {
  const items = Array.from({ length: 1000 }, (_, i) => ({
    id: i,
    name: `Item ${i}`,
    description: `Description for item ${i}`,
  }));

  const [query, setQuery] = useState('');
  const [results, setResults] = useState(items);
  const [isPending, startTransition] = useTransition();

  const handleSearch = (e) => {
    const value = e.target.value;

    // ✅ URGENT: Update input immediately
    setQuery(value);

    // ✅ NON-URGENT: Defer filtering
    startTransition(() => {
      if (!value.trim()) {
        setResults(items);
        return;
      }

      const filtered = items.filter(
        (item) =>
          item.name.toLowerCase().includes(value.toLowerCase()) ||
          item.description.toLowerCase().includes(value.toLowerCase()),
      );
      setResults(filtered);
    });
  };

  return (
    <div>
      <h3>Search Items</h3>

      <input
        type='text'
        value={query}
        onChange={handleSearch}
        placeholder='Search...'
        style={{
          padding: 8,
          width: '100%',
          marginBottom: 8,
        }}
      />

      {isPending && (
        <div
          style={{
            padding: 8,
            backgroundColor: '#dbeafe',
            borderRadius: 4,
            marginBottom: 8,
          }}
        >
          🔍 Searching...
        </div>
      )}

      <p>Found: {results.length} items</p>

      <div style={{ opacity: isPending ? 0.6 : 1 }}>
        {results.slice(0, 50).map((item) => (
          <div
            key={item.id}
            style={{
              padding: 8,
              border: '1px solid #e5e7eb',
              marginBottom: 4,
              borderRadius: 4,
            }}
          >
            <strong>{item.name}</strong>
            <p style={{ margin: 0, fontSize: 12, color: '#6b7280' }}>
              {item.description}
            </p>
          </div>
        ))}
      </div>
    </div>
  );
}

/**
 * Quan sát:
 * - Input luôn responsive, không lag
 * - isPending shows khi filtering
 * - Results fade khi updating
 * - Smooth UX ngay cả với 1000 items
 */

⭐⭐ Level 2: Transition Priority Patterns (25 phút)

jsx
/**
 * 🎯 Mục tiêu: So sánh different approaches
 * ⏱️ Thời gian: 25 phút
 *
 * Scenario: Filter products với multiple controls
 *
 * 🤔 PHÂN TÍCH:
 * Approach A: Tất cả updates trong startTransition
 * Pros: Simple code
 * Cons: Input có thể lag
 *
 * Approach B: Chỉ filtering trong startTransition
 * Pros: Input responsive
 * Cons: Phức tạp hơn
 *
 * Approach C: Debounce + startTransition
 * Pros: Ít filtering calls
 * Cons: Delay trong feedback
 *
 * 💭 BẠN CHỌN GÌ VÀ TẠI SAO?
 */

// 🎯 NHIỆM VỤ CỦA BẠN:
function ProductFilter() {
  // TODO: Implement 3 approaches
  // TODO: Add metrics để compare (render count, input lag)
  // TODO: Document pros/cons từ testing
}
💡 Solution
jsx
/**
 * So sánh 3 approaches cho filtering
 */
function ProductFilterComparison() {
  const [approach, setApproach] = useState('B');

  return (
    <div>
      <h3>Transition Priority Patterns</h3>

      <div style={{ marginBottom: 16 }}>
        <button onClick={() => setApproach('A')}>
          Approach A: All in Transition
        </button>
        <button onClick={() => setApproach('B')}>
          Approach B: Smart Split
        </button>
        <button onClick={() => setApproach('C')}>
          Approach C: Debounce + Transition
        </button>
      </div>

      {approach === 'A' && <ApproachA />}
      {approach === 'B' && <ApproachB />}
      {approach === 'C' && <ApproachC />}
    </div>
  );
}

/**
 * Approach A: Tất cả updates trong transition
 * ❌ ANTI-PATTERN: Input lag
 */
function ApproachA() {
  const products = generateProducts(2000);
  const [query, setQuery] = useState('');
  const [minPrice, setMinPrice] = useState(0);
  const [results, setResults] = useState(products);
  const [isPending, startTransition] = useTransition();

  const handleQueryChange = (e) => {
    const value = e.target.value;

    // ❌ Wrapping input update in transition
    startTransition(() => {
      setQuery(value); // This will lag!

      const filtered = products.filter(
        (p) =>
          p.name.toLowerCase().includes(value.toLowerCase()) &&
          p.price >= minPrice,
      );
      setResults(filtered);
    });
  };

  const handlePriceChange = (e) => {
    const value = Number(e.target.value);

    startTransition(() => {
      setMinPrice(value);

      const filtered = products.filter(
        (p) =>
          p.name.toLowerCase().includes(query.toLowerCase()) &&
          p.price >= value,
      );
      setResults(filtered);
    });
  };

  return (
    <div>
      <h4>❌ Approach A: All in Transition (BAD)</h4>
      <p style={{ color: '#ef4444' }}>
        Notice: Input lags because update is deferred
      </p>

      <input
        value={query}
        onChange={handleQueryChange}
        placeholder='Search (will lag)...'
        style={{ padding: 8, marginRight: 8 }}
      />

      <input
        type='number'
        value={minPrice}
        onChange={handlePriceChange}
        placeholder='Min price'
        style={{ padding: 8, width: 100 }}
      />

      {isPending && <span> Loading...</span>}
      <p>Results: {results.length}</p>
    </div>
  );
}

/**
 * Approach B: Smart split - chỉ heavy work trong transition
 * ✅ RECOMMENDED
 */
function ApproachB() {
  const products = generateProducts(2000);
  const [query, setQuery] = useState('');
  const [minPrice, setMinPrice] = useState(0);
  const [results, setResults] = useState(products);
  const [isPending, startTransition] = useTransition();

  const handleQueryChange = (e) => {
    const value = e.target.value;

    // ✅ URGENT: Update input immediately
    setQuery(value);

    // ✅ NON-URGENT: Defer filtering
    startTransition(() => {
      const filtered = products.filter(
        (p) =>
          p.name.toLowerCase().includes(value.toLowerCase()) &&
          p.price >= minPrice,
      );
      setResults(filtered);
    });
  };

  const handlePriceChange = (e) => {
    const value = Number(e.target.value);

    // ✅ URGENT: Update input immediately
    setMinPrice(value);

    // ✅ NON-URGENT: Defer filtering
    startTransition(() => {
      const filtered = products.filter(
        (p) =>
          p.name.toLowerCase().includes(query.toLowerCase()) &&
          p.price >= value,
      );
      setResults(filtered);
    });
  };

  return (
    <div>
      <h4>✅ Approach B: Smart Split (GOOD)</h4>
      <p style={{ color: '#10b981' }}>Input responsive, filtering deferred</p>

      <input
        value={query}
        onChange={handleQueryChange}
        placeholder='Search (smooth!)...'
        style={{ padding: 8, marginRight: 8 }}
      />

      <input
        type='number'
        value={minPrice}
        onChange={handlePriceChange}
        placeholder='Min price'
        style={{ padding: 8, width: 100 }}
      />

      {isPending && <span style={{ color: '#3b82f6' }}> Filtering...</span>}
      <p>Results: {results.length}</p>

      <div style={{ opacity: isPending ? 0.6 : 1 }}>
        {results.slice(0, 20).map((p) => (
          <div
            key={p.id}
            style={{ padding: 4, borderBottom: '1px solid #eee' }}
          >
            {p.name} - ${p.price}
          </div>
        ))}
      </div>
    </div>
  );
}

/**
 * Approach C: Debounce + Transition
 * ⚠️ Trade-off: Less work but delayed feedback
 */
function ApproachC() {
  const products = generateProducts(2000);
  const [query, setQuery] = useState('');
  const [debouncedQuery, setDebouncedQuery] = useState('');
  const [minPrice, setMinPrice] = useState(0);
  const [isPending, startTransition] = useTransition();

  // Debounce effect
  useEffect(() => {
    const timer = setTimeout(() => {
      startTransition(() => {
        setDebouncedQuery(query);
      });
    }, 300);

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

  const results = products.filter(
    (p) =>
      p.name.toLowerCase().includes(debouncedQuery.toLowerCase()) &&
      p.price >= minPrice,
  );

  const handleQueryChange = (e) => {
    setQuery(e.target.value);
  };

  const handlePriceChange = (e) => {
    const value = Number(e.target.value);
    setMinPrice(value);
  };

  const isTyping = query !== debouncedQuery;

  return (
    <div>
      <h4>⚠️ Approach C: Debounce + Transition</h4>
      <p style={{ color: '#f59e0b' }}>Delayed results but fewer re-renders</p>

      <input
        value={query}
        onChange={handleQueryChange}
        placeholder='Search (300ms delay)...'
        style={{ padding: 8, marginRight: 8 }}
      />

      <input
        type='number'
        value={minPrice}
        onChange={handlePriceChange}
        placeholder='Min price'
        style={{ padding: 8, width: 100 }}
      />

      {(isPending || isTyping) && (
        <span style={{ color: '#f59e0b' }}> Waiting to search...</span>
      )}

      <p>Results: {results.length}</p>
    </div>
  );
}

function generateProducts(count) {
  return Array.from({ length: count }, (_, i) => ({
    id: i,
    name: `Product ${i}`,
    price: Math.floor(Math.random() * 1000),
  }));
}

/**
 * Comparison Summary:
 *
 * Approach A (All in Transition):
 * ❌ Input lag
 * ❌ Poor UX
 * ✅ Simple code
 *
 * Approach B (Smart Split):
 * ✅ Responsive input
 * ✅ Good UX
 * ✅ Immediate feedback
 * ⚠️ More transitions
 *
 * Approach C (Debounce + Transition):
 * ✅ Fewer filtering operations
 * ✅ Responsive input
 * ❌ Delayed results
 * ⚠️ More complex
 *
 * RECOMMENDATION: Approach B for most cases
 */

⭐⭐⭐ Level 3: Real-world Dashboard (40 phút)

jsx
/**
 * 🎯 Mục tiêu: Build dashboard với multiple data views
 * ⏱️ Thời gian: 40 phút
 *
 * 📋 Product Requirements:
 * User Story: "Là analyst, tôi muốn switch giữa các views
 * mà không bị lag, và biết khi nào data đang load"
 *
 * ✅ Acceptance Criteria:
 * - [ ] Tab switching instant
 * - [ ] Loading indicator khi view đang render
 * - [ ] Previous view visible cho đến khi new view ready
 * - [ ] No flashing/jarring transitions
 *
 * 🎨 Technical Constraints:
 * - 3 tabs: Chart, Table, Stats
 * - Each view renders 1000+ data points
 * - Must use useTransition
 *
 * 🚨 Edge Cases cần handle:
 * - Rapid tab switching
 * - View chưa render xong user switch tab
 * - Empty data state
 *
 * 📝 Implementation Checklist:
 * - [ ] Tab navigation
 * - [ ] isPending feedback
 * - [ ] View transitions
 * - [ ] Performance optimization
 */

// 🎯 NHIỆM VỤ CỦA BẠN:
function DataDashboard() {
  // TODO: Implement tabs
  // TODO: Implement transitions
  // TODO: Add loading states
  // TODO: Handle edge cases
}
💡 Solution
jsx
/**
 * Data Dashboard với smooth transitions
 */
function DataDashboard() {
  const [activeView, setActiveView] = useState('chart');
  const [isPending, startTransition] = useTransition();

  // Generate sample data
  const data = Array.from({ length: 1000 }, (_, i) => ({
    id: i,
    value: Math.floor(Math.random() * 100),
    category: ['A', 'B', 'C'][i % 3],
    timestamp: Date.now() - i * 1000,
  }));

  const handleViewChange = (view) => {
    // Use transition to defer heavy view rendering
    startTransition(() => {
      setActiveView(view);
    });
  };

  return (
    <div style={{ maxWidth: 800, margin: '0 auto' }}>
      <h2>📊 Data Dashboard</h2>

      {/* Tab Navigation */}
      <div
        style={{
          display: 'flex',
          gap: 8,
          marginBottom: 16,
          borderBottom: '2px solid #e5e7eb',
          paddingBottom: 8,
        }}
      >
        {['chart', 'table', 'stats'].map((view) => (
          <button
            key={view}
            onClick={() => handleViewChange(view)}
            disabled={isPending}
            style={{
              padding: '8px 16px',
              backgroundColor: activeView === view ? '#3b82f6' : 'white',
              color: activeView === view ? 'white' : '#374151',
              border: '1px solid #d1d5db',
              borderRadius: '4px 4px 0 0',
              cursor: isPending ? 'not-allowed' : 'pointer',
              fontWeight: activeView === view ? 'bold' : 'normal',
              opacity: isPending ? 0.6 : 1,
              transition: 'all 0.2s',
            }}
          >
            {view.charAt(0).toUpperCase() + view.slice(1)}
            {isPending && activeView === view && ' ⏳'}
          </button>
        ))}
      </div>

      {/* Loading Banner */}
      {isPending && (
        <div
          style={{
            padding: 12,
            backgroundColor: '#dbeafe',
            color: '#1e40af',
            borderRadius: 4,
            marginBottom: 16,
            display: 'flex',
            alignItems: 'center',
            gap: 8,
          }}
        >
          <div
            style={{
              width: 16,
              height: 16,
              border: '2px solid #1e40af',
              borderTopColor: 'transparent',
              borderRadius: '50%',
              animation: 'spin 1s linear infinite',
            }}
          />
          Loading {activeView} view...
        </div>
      )}

      {/* View Content */}
      <div
        style={{
          minHeight: 400,
          padding: 16,
          border: '1px solid #e5e7eb',
          borderRadius: 4,
          backgroundColor: 'white',
          opacity: isPending ? 0.5 : 1,
          transition: 'opacity 0.3s',
        }}
      >
        {activeView === 'chart' && <ChartView data={data} />}
        {activeView === 'table' && <TableView data={data} />}
        {activeView === 'stats' && <StatsView data={data} />}
      </div>
    </div>
  );
}

/**
 * Heavy Chart View
 */
function ChartView({ data }) {
  // Simulate heavy rendering
  const startTime = performance.now();
  while (performance.now() - startTime < 100) {
    // Busy wait
  }

  // Simple bar chart visualization
  const chartData = data.slice(0, 50);
  const maxValue = Math.max(...chartData.map((d) => d.value));

  return (
    <div>
      <h3>📈 Chart View</h3>
      <div
        style={{ display: 'flex', alignItems: 'flex-end', gap: 2, height: 200 }}
      >
        {chartData.map((item) => (
          <div
            key={item.id}
            style={{
              flex: 1,
              height: `${(item.value / maxValue) * 100}%`,
              backgroundColor: '#3b82f6',
              minWidth: 2,
              transition: 'height 0.3s',
            }}
            title={`Value: ${item.value}`}
          />
        ))}
      </div>
      <p style={{ marginTop: 16, color: '#6b7280', fontSize: 14 }}>
        Showing {chartData.length} of {data.length} data points
      </p>
    </div>
  );
}

/**
 * Heavy Table View
 */
function TableView({ data }) {
  // Simulate heavy rendering
  const startTime = performance.now();
  while (performance.now() - startTime < 100) {
    // Busy wait
  }

  return (
    <div>
      <h3>📋 Table View</h3>
      <table style={{ width: '100%', borderCollapse: 'collapse' }}>
        <thead>
          <tr style={{ backgroundColor: '#f3f4f6' }}>
            <th
              style={{
                padding: 8,
                textAlign: 'left',
                borderBottom: '2px solid #e5e7eb',
              }}
            >
              ID
            </th>
            <th
              style={{
                padding: 8,
                textAlign: 'left',
                borderBottom: '2px solid #e5e7eb',
              }}
            >
              Value
            </th>
            <th
              style={{
                padding: 8,
                textAlign: 'left',
                borderBottom: '2px solid #e5e7eb',
              }}
            >
              Category
            </th>
          </tr>
        </thead>
        <tbody>
          {data.slice(0, 100).map((item) => (
            <tr key={item.id}>
              <td style={{ padding: 8, borderBottom: '1px solid #e5e7eb' }}>
                {item.id}
              </td>
              <td style={{ padding: 8, borderBottom: '1px solid #e5e7eb' }}>
                {item.value}
              </td>
              <td style={{ padding: 8, borderBottom: '1px solid #e5e7eb' }}>
                {item.category}
              </td>
            </tr>
          ))}
        </tbody>
      </table>
      <p style={{ marginTop: 16, color: '#6b7280', fontSize: 14 }}>
        Showing 100 of {data.length} rows
      </p>
    </div>
  );
}

/**
 * Heavy Stats View
 */
function StatsView({ data }) {
  // Simulate heavy rendering
  const startTime = performance.now();
  while (performance.now() - startTime < 100) {
    // Busy wait
  }

  // Calculate statistics
  const total = data.reduce((sum, item) => sum + item.value, 0);
  const average = total / data.length;
  const max = Math.max(...data.map((d) => d.value));
  const min = Math.min(...data.map((d) => d.value));

  const byCategory = data.reduce((acc, item) => {
    acc[item.category] = (acc[item.category] || 0) + 1;
    return acc;
  }, {});

  return (
    <div>
      <h3>📊 Statistics View</h3>

      <div
        style={{
          display: 'grid',
          gridTemplateColumns: 'repeat(2, 1fr)',
          gap: 16,
          marginBottom: 24,
        }}
      >
        <StatCard
          title='Total'
          value={total.toLocaleString()}
        />
        <StatCard
          title='Average'
          value={average.toFixed(2)}
        />
        <StatCard
          title='Maximum'
          value={max}
        />
        <StatCard
          title='Minimum'
          value={min}
        />
      </div>

      <h4>By Category</h4>
      <div style={{ display: 'flex', gap: 16 }}>
        {Object.entries(byCategory).map(([category, count]) => (
          <div
            key={category}
            style={{
              flex: 1,
              padding: 16,
              backgroundColor: '#f3f4f6',
              borderRadius: 8,
              textAlign: 'center',
            }}
          >
            <div style={{ fontSize: 32, fontWeight: 'bold', color: '#3b82f6' }}>
              {count}
            </div>
            <div style={{ fontSize: 14, color: '#6b7280', marginTop: 4 }}>
              Category {category}
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

function StatCard({ title, value }) {
  return (
    <div
      style={{
        padding: 20,
        backgroundColor: '#f9fafb',
        borderRadius: 8,
        border: '1px solid #e5e7eb',
      }}
    >
      <div style={{ fontSize: 14, color: '#6b7280', marginBottom: 8 }}>
        {title}
      </div>
      <div style={{ fontSize: 28, fontWeight: 'bold', color: '#111827' }}>
        {value}
      </div>
    </div>
  );
}

// Add CSS animation
const style = document.createElement('style');
style.textContent = `
  @keyframes spin {
    to { transform: rotate(360deg); }
  }
`;
document.head.appendChild(style);

/**
 * UX Features:
 * ✅ Instant tab highlighting
 * ✅ Loading banner during transition
 * ✅ Content fades smoothly
 * ✅ Previous view visible until new ready
 * ✅ Disabled tabs during transition (prevent rapid switching)
 * ✅ Clear loading state with spinner
 *
 * Performance:
 * - Each view takes ~100ms to render
 * - Without transition: UI freezes 100ms
 * - With transition: Tab switches instantly, content loads in background
 */

⭐⭐⭐⭐ Level 4: Transition Orchestration (60 phút)

jsx
/**
 * 🎯 Mục tiêu: Coordinate multiple transitions
 * ⏱️ Thời gian: 60 phút
 *
 * 🏗️ PHASE 1: Research & Design (20 phút)
 *
 * Problem: Multi-step form với heavy validation
 * - Step 1: Personal Info
 * - Step 2: Address (fetch cities based on country)
 * - Step 3: Payment (validate card)
 * - Step 4: Review
 *
 * Questions:
 * 1. Nên dùng transition cho step navigation không?
 * 2. Nên dùng transition cho city fetching không?
 * 3. Làm sao prevent navigation khi data đang load?
 *
 * ADR Template:
 * - Context: Multi-step form với async operations
 * - Decision: Use transitions for step changes, not for data fetching
 * - Rationale: Step change = UI work, fetch = network work
 * - Consequences: Better UX, clearer loading states
 * - Alternatives: Loading overlays, disabled buttons
 *
 * 💻 PHASE 2: Implementation (30 phút)
 * 🧪 PHASE 3: Testing (10 phút)
 */
💡 Solution
jsx
/**
 * Multi-step Form với Transition Orchestration
 */
function MultiStepForm() {
  const [currentStep, setCurrentStep] = useState(1);
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    country: '',
    city: '',
    cardNumber: '',
  });
  const [isPending, startTransition] = useTransition();

  const totalSteps = 4;

  const handleNext = () => {
    // Validate current step
    if (!validateStep(currentStep, formData)) {
      alert('Please fill all required fields');
      return;
    }

    // ✅ Use transition for step navigation
    startTransition(() => {
      setCurrentStep((prev) => Math.min(prev + 1, totalSteps));
    });
  };

  const handlePrevious = () => {
    startTransition(() => {
      setCurrentStep((prev) => Math.max(prev - 1, 1));
    });
  };

  const handleFieldChange = (field, value) => {
    setFormData((prev) => ({ ...prev, [field]: value }));
  };

  return (
    <div style={{ maxWidth: 600, margin: '0 auto', padding: 20 }}>
      <h2>Multi-Step Form</h2>

      {/* Progress Bar */}
      <div style={{ marginBottom: 24 }}>
        <div
          style={{
            display: 'flex',
            justifyContent: 'space-between',
            marginBottom: 8,
          }}
        >
          {Array.from({ length: totalSteps }).map((_, i) => (
            <div
              key={i}
              style={{
                flex: 1,
                height: 4,
                backgroundColor: i < currentStep ? '#3b82f6' : '#e5e7eb',
                marginRight: i < totalSteps - 1 ? 4 : 0,
                transition: 'background-color 0.3s',
              }}
            />
          ))}
        </div>
        <div style={{ fontSize: 14, color: '#6b7280' }}>
          Step {currentStep} of {totalSteps}
          {isPending && ' (Loading...)'}
        </div>
      </div>

      {/* Step Content */}
      <div
        style={{
          minHeight: 300,
          padding: 20,
          border: '1px solid #e5e7eb',
          borderRadius: 8,
          backgroundColor: 'white',
          opacity: isPending ? 0.6 : 1,
          transition: 'opacity 0.3s',
        }}
      >
        {currentStep === 1 && (
          <StepPersonalInfo
            data={formData}
            onChange={handleFieldChange}
          />
        )}
        {currentStep === 2 && (
          <StepAddress
            data={formData}
            onChange={handleFieldChange}
          />
        )}
        {currentStep === 3 && (
          <StepPayment
            data={formData}
            onChange={handleFieldChange}
          />
        )}
        {currentStep === 4 && <StepReview data={formData} />}
      </div>

      {/* Navigation */}
      <div
        style={{
          display: 'flex',
          justifyContent: 'space-between',
          marginTop: 16,
        }}
      >
        <button
          onClick={handlePrevious}
          disabled={currentStep === 1 || isPending}
          style={{
            padding: '8px 16px',
            backgroundColor: currentStep === 1 ? '#e5e7eb' : 'white',
            border: '1px solid #d1d5db',
            borderRadius: 4,
            cursor: currentStep === 1 || isPending ? 'not-allowed' : 'pointer',
          }}
        >
          Previous
        </button>

        <button
          onClick={handleNext}
          disabled={currentStep === totalSteps || isPending}
          style={{
            padding: '8px 16px',
            backgroundColor: '#3b82f6',
            color: 'white',
            border: 'none',
            borderRadius: 4,
            cursor:
              currentStep === totalSteps || isPending
                ? 'not-allowed'
                : 'pointer',
            opacity: currentStep === totalSteps || isPending ? 0.5 : 1,
          }}
        >
          {currentStep === totalSteps ? 'Submit' : 'Next'}
        </button>
      </div>
    </div>
  );
}

function StepPersonalInfo({ data, onChange }) {
  // Simulate heavy component
  const startTime = performance.now();
  while (performance.now() - startTime < 50) {}

  return (
    <div>
      <h3>Personal Information</h3>
      <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
        <input
          type='text'
          placeholder='Full Name'
          value={data.name}
          onChange={(e) => onChange('name', e.target.value)}
          style={{ padding: 8, border: '1px solid #d1d5db', borderRadius: 4 }}
        />
        <input
          type='email'
          placeholder='Email'
          value={data.email}
          onChange={(e) => onChange('email', e.target.value)}
          style={{ padding: 8, border: '1px solid #d1d5db', borderRadius: 4 }}
        />
      </div>
    </div>
  );
}

function StepAddress({ data, onChange }) {
  const [cities, setCities] = useState([]);
  const [loadingCities, setLoadingCities] = useState(false);

  // Simulate heavy component
  const startTime = performance.now();
  while (performance.now() - startTime < 50) {}

  // ⚠️ DON'T use transition for data fetching!
  // Use regular async state management
  useEffect(() => {
    if (!data.country) {
      setCities([]);
      return;
    }

    setLoadingCities(true);

    // Simulate API call
    setTimeout(() => {
      const mockCities = {
        US: ['New York', 'Los Angeles', 'Chicago'],
        UK: ['London', 'Manchester', 'Birmingham'],
        VN: ['Hanoi', 'Ho Chi Minh', 'Da Nang'],
      };
      setCities(mockCities[data.country] || []);
      setLoadingCities(false);
    }, 500);
  }, [data.country]);

  return (
    <div>
      <h3>Address</h3>
      <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
        <select
          value={data.country}
          onChange={(e) => onChange('country', e.target.value)}
          style={{ padding: 8, border: '1px solid #d1d5db', borderRadius: 4 }}
        >
          <option value=''>Select Country</option>
          <option value='US'>United States</option>
          <option value='UK'>United Kingdom</option>
          <option value='VN'>Vietnam</option>
        </select>

        <select
          value={data.city}
          onChange={(e) => onChange('city', e.target.value)}
          disabled={!data.country || loadingCities}
          style={{
            padding: 8,
            border: '1px solid #d1d5db',
            borderRadius: 4,
            opacity: !data.country || loadingCities ? 0.5 : 1,
          }}
        >
          <option value=''>
            {loadingCities ? 'Loading cities...' : 'Select City'}
          </option>
          {cities.map((city) => (
            <option
              key={city}
              value={city}
            >
              {city}
            </option>
          ))}
        </select>
      </div>
    </div>
  );
}

function StepPayment({ data, onChange }) {
  // Simulate heavy component
  const startTime = performance.now();
  while (performance.now() - startTime < 50) {}

  return (
    <div>
      <h3>Payment Information</h3>
      <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
        <input
          type='text'
          placeholder='Card Number'
          value={data.cardNumber}
          onChange={(e) => onChange('cardNumber', e.target.value)}
          maxLength={16}
          style={{ padding: 8, border: '1px solid #d1d5db', borderRadius: 4 }}
        />
        <p style={{ fontSize: 12, color: '#6b7280' }}>
          Enter 16-digit card number
        </p>
      </div>
    </div>
  );
}

function StepReview({ data }) {
  // Simulate heavy component
  const startTime = performance.now();
  while (performance.now() - startTime < 50) {}

  return (
    <div>
      <h3>Review Your Information</h3>
      <div
        style={{
          display: 'flex',
          flexDirection: 'column',
          gap: 12,
          padding: 16,
          backgroundColor: '#f9fafb',
          borderRadius: 4,
        }}
      >
        <ReviewRow
          label='Name'
          value={data.name}
        />
        <ReviewRow
          label='Email'
          value={data.email}
        />
        <ReviewRow
          label='Country'
          value={data.country}
        />
        <ReviewRow
          label='City'
          value={data.city}
        />
        <ReviewRow
          label='Card'
          value={
            data.cardNumber ? `**** **** **** ${data.cardNumber.slice(-4)}` : ''
          }
        />
      </div>
    </div>
  );
}

function ReviewRow({ label, value }) {
  return (
    <div style={{ display: 'flex', justifyContent: 'space-between' }}>
      <strong>{label}:</strong>
      <span>{value || '—'}</span>
    </div>
  );
}

function validateStep(step, data) {
  switch (step) {
    case 1:
      return data.name && data.email;
    case 2:
      return data.country && data.city;
    case 3:
      return data.cardNumber && data.cardNumber.length === 16;
    default:
      return true;
  }
}

/**
 * Key Decisions:
 *
 * 1. ✅ Use transition for step navigation
 *    - Step changes involve rendering different components
 *    - This is "UI work" perfect for transitions
 *    - Keeps navigation buttons responsive
 *
 * 2. ❌ DON'T use transition for data fetching
 *    - City loading is "network work", not UI work
 *    - Use regular loading states
 *    - Clear separation of concerns
 *
 * 3. ✅ Disable navigation during transition
 *    - Prevents race conditions
 *    - Clear UX - user knows something is happening
 *
 * 4. ✅ Show progress clearly
 *    - Progress bar
 *    - Step indicators
 *    - Loading states for async operations
 */

⭐⭐⭐⭐⭐ Level 5: Advanced Transition Patterns (90 phút)

jsx
/**
 * 🎯 Mục tiêu: Production-ready transition management
 * ⏱️ Thời gian: 90 phút
 *
 * 📋 Feature Specification:
 * Build reusable transition utilities:
 * - useOptimisticTransition - Optimistic updates
 * - useQueuedTransitions - Sequential transitions
 * - useTransitionGroup - Multiple concurrent transitions
 *
 * 🏗️ Technical Design Doc:
 * 1. Hook Architecture
 *    - Composable hooks
 *    - Clear API
 *    - TypeScript-ready
 *
 * 2. State Management
 *    - Track multiple transitions
 *    - Priority handling
 *    - Cancellation support
 *
 * 3. Error Handling
 *    - Rollback on failure
 *    - Error boundaries
 *    - User feedback
 *
 * ✅ Production Checklist:
 * - [ ] Comprehensive error handling
 * - [ ] Loading states
 * - [ ] Optimistic updates
 * - [ ] Rollback capability
 * - [ ] Clear documentation
 * - [ ] Usage examples
 */
💡 Solution
jsx
/**
 * Advanced Transition Utilities
 * Production-ready patterns for complex UIs
 */

/**
 * useOptimisticTransition - Update UI optimistically, rollback on error
 */
function useOptimisticTransition() {
  const [isPending, startTransition] = useTransition();
  const [isOptimistic, setIsOptimistic] = useState(false);

  const execute = useCallback(
    async (optimisticUpdate, actualUpdate, onError) => {
      // Step 1: Apply optimistic update immediately
      setIsOptimistic(true);
      optimisticUpdate();

      // Step 2: Start transition for actual update
      startTransition(async () => {
        try {
          await actualUpdate();
          setIsOptimistic(false);
        } catch (error) {
          // Step 3: Rollback on error
          setIsOptimistic(false);
          onError?.(error);
        }
      });
    },
    [],
  );

  return {
    isPending,
    isOptimistic,
    execute,
  };
}

/**
 * useQueuedTransitions - Execute transitions sequentially
 */
function useQueuedTransitions() {
  const [queue, setQueue] = useState([]);
  const [isPending, startTransition] = useTransition();
  const [currentIndex, setCurrentIndex] = useState(0);

  useEffect(() => {
    if (queue.length === 0 || currentIndex >= queue.length) return;

    const current = queue[currentIndex];

    startTransition(async () => {
      try {
        await current();
        setCurrentIndex((prev) => prev + 1);
      } catch (error) {
        console.error('Transition failed:', error);
        // Clear queue on error
        setQueue([]);
        setCurrentIndex(0);
      }
    });
  }, [queue, currentIndex]);

  const enqueue = useCallback((transition) => {
    setQueue((prev) => [...prev, transition]);
  }, []);

  const clear = useCallback(() => {
    setQueue([]);
    setCurrentIndex(0);
  }, []);

  return {
    enqueue,
    clear,
    isPending,
    queueLength: queue.length,
    currentIndex,
  };
}

/**
 * useTransitionGroup - Manage multiple named transitions
 */
function useTransitionGroup() {
  const [transitions, setTransitions] = useState({});

  const start = useCallback(
    (name, callback) => {
      const [isPending, startTransition] = useTransition();

      setTransitions((prev) => ({
        ...prev,
        [name]: { isPending, startTransition },
      }));

      const transition = transitions[name];
      if (transition) {
        transition.startTransition(callback);
      }
    },
    [transitions],
  );

  const isPending = useCallback(
    (name) => {
      return transitions[name]?.isPending || false;
    },
    [transitions],
  );

  const anyPending = useCallback(() => {
    return Object.values(transitions).some((t) => t.isPending);
  }, [transitions]);

  return {
    start,
    isPending,
    anyPending,
  };
}

// ===============================================
// DEMO APPLICATIONS
// ===============================================

/**
 * Demo 1: Optimistic Like Button
 */
function OptimisticLikeButton() {
  const [likes, setLikes] = useState(42);
  const [isLiked, setIsLiked] = useState(false);
  const { isPending, isOptimistic, execute } = useOptimisticTransition();

  const handleLike = () => {
    const previousLikes = likes;
    const previousIsLiked = isLiked;

    execute(
      // Optimistic update - instant
      () => {
        setLikes((prev) => (isLiked ? prev - 1 : prev + 1));
        setIsLiked((prev) => !prev);
      },
      // Actual update - async
      async () => {
        await new Promise((resolve) => setTimeout(resolve, 1000));

        // Simulate occasional failure
        if (Math.random() < 0.2) {
          throw new Error('Failed to like');
        }
      },
      // Rollback on error
      (error) => {
        alert('Failed to like. Please try again.');
        setLikes(previousLikes);
        setIsLiked(previousIsLiked);
      },
    );
  };

  return (
    <div style={{ padding: 20 }}>
      <h3>Optimistic Like Button</h3>
      <button
        onClick={handleLike}
        disabled={isPending}
        style={{
          padding: '12px 24px',
          fontSize: 16,
          backgroundColor: isLiked ? '#ef4444' : '#e5e7eb',
          color: isLiked ? 'white' : '#374151',
          border: 'none',
          borderRadius: 8,
          cursor: isPending ? 'not-allowed' : 'pointer',
          opacity: isPending ? 0.6 : 1,
          display: 'flex',
          alignItems: 'center',
          gap: 8,
        }}
      >
        <span style={{ fontSize: 20 }}>{isLiked ? '❤️' : '🤍'}</span>
        <span>
          {likes} {isOptimistic && '(saving...)'}
        </span>
      </button>
      <p style={{ fontSize: 12, color: '#6b7280', marginTop: 8 }}>
        20% chance of failure to demo rollback
      </p>
    </div>
  );
}

/**
 * Demo 2: Sequential Animation Queue
 */
function AnimationQueue() {
  const [steps, setSteps] = useState([]);
  const { enqueue, clear, isPending, queueLength, currentIndex } =
    useQueuedTransitions();

  const addAnimation = (name) => {
    const animation = async () => {
      setSteps((prev) => [...prev, `${name} started`]);
      await new Promise((resolve) => setTimeout(resolve, 1000));
      setSteps((prev) => [...prev, `${name} completed`]);
    };

    enqueue(animation);
  };

  const handleClear = () => {
    clear();
    setSteps([]);
  };

  return (
    <div style={{ padding: 20 }}>
      <h3>Sequential Animation Queue</h3>

      <div style={{ display: 'flex', gap: 8, marginBottom: 16 }}>
        <button
          onClick={() => addAnimation('Fade In')}
          disabled={isPending}
        >
          Add Fade In
        </button>
        <button
          onClick={() => addAnimation('Slide')}
          disabled={isPending}
        >
          Add Slide
        </button>
        <button
          onClick={() => addAnimation('Zoom')}
          disabled={isPending}
        >
          Add Zoom
        </button>
        <button onClick={handleClear}>Clear Queue</button>
      </div>

      <div
        style={{
          padding: 16,
          backgroundColor: '#f3f4f6',
          borderRadius: 8,
          minHeight: 100,
        }}
      >
        <p>Queue: {queueLength} items</p>
        <p>
          Current: {currentIndex + 1} of {queueLength}
        </p>
        <p>Status: {isPending ? 'Running...' : 'Idle'}</p>

        <div style={{ marginTop: 12, fontSize: 12 }}>
          {steps.map((step, i) => (
            <div
              key={i}
              style={{ color: '#6b7280' }}
            >
              {step}
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

/**
 * Demo 3: Multi-panel Dashboard
 */
function MultiPanelDashboard() {
  const [panels, setPanels] = useState({
    sales: { visible: true, data: [] },
    users: { visible: false, data: [] },
    revenue: { visible: false, data: [] },
  });

  const transitions = useTransitionGroup();

  const togglePanel = (panelName) => {
    transitions.start(panelName, () => {
      setPanels((prev) => ({
        ...prev,
        [panelName]: {
          ...prev[panelName],
          visible: !prev[panelName].visible,
          data: generateData(100),
        },
      }));
    });
  };

  return (
    <div style={{ padding: 20 }}>
      <h3>Multi-panel Dashboard</h3>

      <div style={{ display: 'flex', gap: 8, marginBottom: 16 }}>
        {Object.keys(panels).map((panel) => (
          <button
            key={panel}
            onClick={() => togglePanel(panel)}
            disabled={transitions.isPending(panel)}
            style={{
              padding: '8px 16px',
              backgroundColor: panels[panel].visible ? '#3b82f6' : '#e5e7eb',
              color: panels[panel].visible ? 'white' : '#374151',
              border: 'none',
              borderRadius: 4,
              cursor: 'pointer',
            }}
          >
            {panel.charAt(0).toUpperCase() + panel.slice(1)}
            {transitions.isPending(panel) && ' ⏳'}
          </button>
        ))}
      </div>

      <div
        style={{
          display: 'grid',
          gridTemplateColumns: 'repeat(3, 1fr)',
          gap: 16,
        }}
      >
        {Object.entries(panels).map(([name, panel]) => (
          <div
            key={name}
            style={{
              padding: 16,
              border: '1px solid #e5e7eb',
              borderRadius: 8,
              minHeight: 200,
              opacity: panel.visible ? 1 : 0.3,
              transition: 'opacity 0.3s',
            }}
          >
            <h4>{name.charAt(0).toUpperCase() + name.slice(1)}</h4>
            {panel.visible && (
              <div>
                {panel.data.slice(0, 5).map((item, i) => (
                  <div
                    key={i}
                    style={{ padding: 4, fontSize: 12 }}
                  >
                    Data point {i + 1}: {item}
                  </div>
                ))}
              </div>
            )}
          </div>
        ))}
      </div>
    </div>
  );
}

function generateData(count) {
  return Array.from({ length: count }, () => Math.floor(Math.random() * 100));
}

/**
 * Production Patterns Demonstrated:
 *
 * 1. useOptimisticTransition:
 *    ✅ Instant feedback
 *    ✅ Graceful rollback
 *    ✅ Error handling
 *    ✅ Clear pending states
 *
 * 2. useQueuedTransitions:
 *    ✅ Sequential execution
 *    ✅ Queue management
 *    ✅ Cancellation support
 *    ✅ Progress tracking
 *
 * 3. useTransitionGroup:
 *    ✅ Multiple concurrent transitions
 *    ✅ Named transitions
 *    ✅ Individual status tracking
 *    ✅ Global pending state
 *
 * These patterns solve real production problems:
 * - Like buttons, favorites, bookmarks
 * - Multi-step processes
 * - Complex dashboards
 * - Coordinated UI updates
 */

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

Bảng So Sánh: Approaches cho Responsive UI

ApproachProsConsWhen to Use
No Optimization✅ Simple
✅ No extra code
❌ UI freezes
❌ Poor UX
❌ Janky inputs
Never in production
useTransition✅ Declarative
✅ Built-in to React
✅ Clear intent
✅ isPending state
❌ Learning curve
❌ Not for async I/O
❌ React 18+ only
Heavy UI updates
Search/filter
Tab switching
debounce✅ Reduces work
✅ Simple to implement
✅ Works in any React version
❌ Delay in feedback
❌ Doesn't solve render blocking
❌ Extra dependency
API calls
Search suggestions
Autosave
useMemo✅ Prevents re-computation
✅ Optimizes renders
❌ Memory overhead
❌ Doesn't help with initial render
❌ Can be overused
Expensive calculations
Derived state
Web Workers✅ True parallelism
✅ Non-blocking
❌ Complex setup
❌ Communication overhead
❌ Limited API access
Heavy computations
Data processing

Trade-offs Matrix: useTransition

AspectWithout useTransitionWith useTransition
Input Responsiveness❌ Blocked during render✅ Always responsive
Perceived Performance❌ Feels slow✅ Feels instant
Actual PerformanceSameSame (slightly slower due to coordination)
Code Complexity✅ Simple⚠️ Moderate
Loading StatesManual✅ Built-in (isPending)
Interruption❌ Cannot interrupt✅ Auto-interrupts
Browser SupportAllReact 18+

Decision Tree: When to Use useTransition

START: Should I use useTransition?

├─ Update blocks UI for >16ms?
│  ├─ NO → ✅ Skip optimization (premature optimization)
│  └─ YES → Continue

├─ Is this user input (typing, clicking)?
│  ├─ YES → ❌ DON'T wrap input update in transition
│  └─ NO → Continue

├─ Is this network request?
│  ├─ YES → ❌ Use regular async state, NOT transition
│  └─ NO → Continue

├─ Is this heavy UI rendering?
│  ├─ YES → ✅ Use useTransition
│  └─ NO → Continue

├─ Can I optimize with useMemo/React.memo first?
│  ├─ YES → Try optimization first, then transition if needed
│  └─ NO → ✅ Use useTransition

└─ Running React 18+?
   ├─ YES → ✅ Use useTransition
   └─ NO → Use debounce or upgrade React

Pattern Comparison: Search Implementation

jsx
// Pattern 1: No optimization ❌
function SearchNoOpt() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);

  const handleChange = (e) => {
    setQuery(e.target.value);
    setResults(filterHeavy(e.target.value)); // Blocks UI!
  };

  return (
    <input
      value={query}
      onChange={handleChange}
    />
  );
}

// Pattern 2: useTransition ✅
function SearchWithTransition() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [isPending, startTransition] = useTransition();

  const handleChange = (e) => {
    setQuery(e.target.value); // Instant
    startTransition(() => {
      setResults(filterHeavy(e.target.value)); // Deferred
    });
  };

  return (
    <>
      <input
        value={query}
        onChange={handleChange}
      />
      {isPending && <Spinner />}
      <Results data={results} />
    </>
  );
}

// Pattern 3: useMemo ⚠️
function SearchWithMemo() {
  const [query, setQuery] = useState('');

  const results = useMemo(() => filterHeavy(query), [query]);

  return (
    <>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
      />
      <Results data={results} />
    </>
  );
  // Note: Input still blocks on first filter!
  // useMemo prevents re-filter, not initial filter
}

// Pattern 4: Debounce ⚠️
function SearchWithDebounce() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);

  useEffect(() => {
    const timer = setTimeout(() => {
      setResults(filterHeavy(query));
    }, 300);

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

  return (
    <>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
      />
      <Results data={results} />
    </>
  );
  // Input responsive BUT delayed results
}

// Pattern 5: Hybrid (Best!) ✅
function SearchHybrid() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [isPending, startTransition] = useTransition();

  // Memoize expensive filtering
  const filter = useCallback((q) => {
    return filterHeavy(q);
  }, []);

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

    startTransition(() => {
      setResults(filter(e.target.value));
    });
  };

  return (
    <>
      <input
        value={query}
        onChange={handleChange}
      />
      {isPending && <Spinner />}
      <Results data={results} />
    </>
  );
}

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

Bug 1: Wrapping Wrong Updates

jsx
/**
 * ❌ BUG: Input lag vì wrap sai update
 *
 * Symptom: User types nhưng characters xuất hiện chậm
 * Root cause: Wrapped urgent update trong transition
 */
function BuggySearch() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [isPending, startTransition] = useTransition();

  const handleChange = (e) => {
    // ❌ Wrapping cả 2 updates!
    startTransition(() => {
      setQuery(e.target.value); // This should be urgent!
      setResults(filterItems(e.target.value));
    });
  };

  return (
    <input
      value={query}
      onChange={handleChange}
    />
  );
}

// ❓ CÂU HỎI:
// 1. Tại sao input bị lag?
// 2. Update nào nên urgent, update nào nên non-urgent?
// 3. Fix như thế nào?

💡 Giải thích:

  1. Tại sao lag:

    • setQuery được wrap trong startTransition
    • Input update bị defer → Characters xuất hiện chậm
    • User experience tệ!
  2. Phân loại updates:

    • URGENT: setQuery - Người dùng PHẢI thấy gõ gì ngay
    • NON-URGENT: setResults - Kết quả có thể đợi
  3. Fix:

    jsx
    function FixedSearch() {
      const [query, setQuery] = useState('');
      const [results, setResults] = useState([]);
      const [isPending, startTransition] = useTransition();
    
      const handleChange = (e) => {
        const value = e.target.value;
    
        // ✅ URGENT: Update immediately
        setQuery(value);
    
        // ✅ NON-URGENT: Defer filtering
        startTransition(() => {
          setResults(filterItems(value));
        });
      };
    
      return (
        <input
          value={query}
          onChange={handleChange}
        />
      );
    }

Rule of Thumb:

  • Direct user input → URGENT (không wrap)
  • Derived results → NON-URGENT (wrap trong transition)

Bug 2: Using Transition for Async Operations

jsx
/**
 * ❌ BUG: startTransition cho async fetch
 *
 * Symptom: isPending không work correctly, data inconsistent
 * Root cause: Transition không designed cho async I/O
 */
function BuggyDataFetch() {
  const [data, setData] = useState(null);
  const [isPending, startTransition] = useTransition();

  const loadData = () => {
    // ❌ Using transition for network request!
    startTransition(async () => {
      const response = await fetch('/api/data');
      const json = await response.json();
      setData(json);
    });
  };

  return (
    <div>
      <button onClick={loadData}>Load Data</button>
      {isPending && <div>Loading...</div>}
      {data && <div>{JSON.stringify(data)}</div>}
    </div>
  );
}

// ❓ CÂU HỎI:
// 1. Tại sao isPending không reliable?
// 2. useTransition dành cho gì?
// 3. Đúng pattern là gì?

💡 Giải thích:

  1. Tại sao isPending không work:

    • isPending tracks React rendering, KHÔNG track async operations
    • startTransition callback execute sync, nhưng fetch là async
    • isPending có thể become false TRƯỚC KHI fetch completes!
  2. useTransition dành cho:

    • ✅ Heavy UI rendering (filtering lists, sorting)
    • Synchronous state updates that trigger heavy renders
    • ❌ KHÔNG cho network requests
    • ❌ KHÔNG cho async I/O operations
  3. Correct pattern:

    jsx
    function FixedDataFetch() {
      const [data, setData] = useState(null);
      const [loading, setLoading] = useState(false);
    
      const loadData = async () => {
        // ✅ Manual loading state cho async
        setLoading(true);
    
        try {
          const response = await fetch('/api/data');
          const json = await response.json();
    
          // Optional: Use transition for heavy rendering
          // startTransition(() => {
          //   setData(json);
          // });
    
          setData(json);
        } catch (error) {
          console.error(error);
        } finally {
          setLoading(false);
        }
      };
    
      return (
        <div>
          <button
            onClick={loadData}
            disabled={loading}
          >
            Load Data
          </button>
          {loading && <div>Loading...</div>}
          {data && <div>{JSON.stringify(data)}</div>}
        </div>
      );
    }

Remember:

useTransition: Cho UI rendering work
Manual state: Cho network/async I/O work

Bug 3: Multiple Transitions Conflict

jsx
/**
 * ❌ BUG: Race condition với multiple transitions
 *
 * Symptom: Results không match query
 * Root cause: Misunderstanding transition interruption
 */
function BuggyMultiFilter() {
  const [query, setQuery] = useState('');
  const [category, setCategory] = useState('all');
  const [results, setResults] = useState([]);
  const [isPending1, startTransition1] = useTransition();
  const [isPending2, startTransition2] = useTransition();

  const handleQueryChange = (e) => {
    setQuery(e.target.value);

    // ❌ Separate transition for query
    startTransition1(() => {
      setResults(filter(e.target.value, category));
    });
  };

  const handleCategoryChange = (e) => {
    setCategory(e.target.value);

    // ❌ Separate transition for category
    startTransition2(() => {
      setResults(filter(query, e.target.value));
    });
  };

  return (
    <>
      <input
        value={query}
        onChange={handleQueryChange}
      />
      <select
        value={category}
        onChange={handleCategoryChange}
      >
        <option value='all'>All</option>
        <option value='active'>Active</option>
      </select>
      {(isPending1 || isPending2) && <div>Loading...</div>}
      <div>{results.length} results</div>
    </>
  );
}

// ❓ CÂU HỎI:
// 1. Tại sao results có thể sai?
// 2. Multiple useTransition có conflict không?
// 3. Best practice là gì?

💡 Giải thích:

  1. Tại sao results sai:

    User types "a" → transition1 starts filtering
    User changes category → transition2 starts filtering
    
    Transition1 có thể complete AFTER transition2
    → Results from transition1 overwrite transition2
    → Results don't match current filters!
  2. Multiple transitions:

    • Multiple useTransition hooks = multiple independent transitions
    • They DON'T coordinate với nhau
    • Last setState wins, nhưng không guarantee order
  3. Best practices:

    jsx
    // ✅ Solution 1: Single transition
    function FixedSingleTransition() {
      const [query, setQuery] = useState('');
      const [category, setCategory] = useState('all');
      const [results, setResults] = useState([]);
      const [isPending, startTransition] = useTransition();
    
      const performFilter = (newQuery, newCategory) => {
        startTransition(() => {
          setResults(filter(newQuery, newCategory));
        });
      };
    
      const handleQueryChange = (e) => {
        const value = e.target.value;
        setQuery(value);
        performFilter(value, category);
      };
    
      const handleCategoryChange = (e) => {
        const value = e.target.value;
        setCategory(value);
        performFilter(query, value);
      };
    
      return /* ... */;
    }
    
    // ✅ Solution 2: useMemo (simpler!)
    function FixedWithMemo() {
      const [query, setQuery] = useState('');
      const [category, setCategory] = useState('all');
    
      const results = useMemo(() => filter(query, category), [query, category]);
    
      return /* ... */;
    }

Key Lesson:

  • Prefer single transition cho coordinated updates
  • Consider useMemo cho derived state
  • Keep transitions simple and predictable

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

Knowledge Check

  • [ ] Tôi hiểu sự khác biệt urgent vs non-urgent updates
  • [ ] Tôi biết khi nào dùng useTransition và khi nào KHÔNG
  • [ ] Tôi hiểu isPending state và cách dùng để show feedback
  • [ ] Tôi biết useTransition không dành cho async I/O
  • [ ] Tôi có thể identify updates nào nên wrap trong startTransition
  • [ ] Tôi hiểu trade-offs giữa useTransition và debounce
  • [ ] Tôi biết handle multiple transitions properly

Code Review Checklist

useTransition Usage:

  • [ ] Chỉ wrap non-urgent updates (filtering, sorting)
  • [ ] Input updates KHÔNG wrap trong startTransition
  • [ ] Network requests dùng manual loading state
  • [ ] isPending được dùng để show feedback
  • [ ] Transitions không conflict với nhau

Performance:

  • [ ] Heavy rendering được defer properly
  • [ ] UI responsive during transitions
  • [ ] No unnecessary transitions (premature optimization)
  • [ ] Combined với useMemo nếu cần

UX:

  • [ ] Clear loading indicators
  • [ ] Smooth transitions
  • [ ] No jarring state changes
  • [ ] Instant feedback cho user input

🏠 BÀI TẬP VỀ NHÀ

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

1. Search Comparison:

  • Implement search với 3 approaches:
    • No optimization
    • With useTransition
    • With debounce
  • Measure và compare:
    • Input responsiveness
    • Time to show results
    • Total render count
  • Document findings

2. Tab Switching:

  • Build tab interface với 3 tabs
  • Each tab có heavy content (1000+ items)
  • Implement với useTransition
  • Add proper loading states

Nâng cao (60 phút)

1. E-commerce Filter:

  • Multi-criteria product filtering:
    • Search query
    • Price range
    • Categories
    • Rating
  • Use useTransition properly
  • Handle rapid filter changes
  • Optimize performance

2. Optimistic Updates:

  • Implement like/bookmark feature
  • Use optimistic updates pattern
  • Handle errors gracefully
  • Provide clear feedback

📚 TÀI LIỆU THAM KHẢO

Bắt buộc đọc

  1. useTransition - React Docs

  2. Patterns for useTransition

Đọc thêm

  1. Concurrent UI Patterns

  2. Performance Optimization


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

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

  • Ngày 46: Concurrent Rendering concepts
  • Ngày 31-34: Performance optimization (useMemo, useCallback)
  • Ngày 16-20: useEffect và async operations
  • Ngày 11-14: useState và state updates

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

  • Ngày 48: useDeferredValue - Alternative approach
  • Ngày 49: Suspense - Declarative loading states
  • Ngày 50: Error Boundaries - Error handling
  • Next.js module: Server Components với transitions

💡 SENIOR INSIGHTS

Cân Nhắc Production

1. When to Use useTransition:

✅ USE:
- Heavy list filtering/sorting
- Tab switching với complex content
- Data visualization updates
- Search results rendering

❌ DON'T USE:
- Network requests
- File I/O
- Database queries
- WebSocket messages
- Simple state updates (<16ms)

2. Performance Monitoring:

jsx
// Track transition performance
const [isPending, startTransition] = useTransition();

useEffect(() => {
  if (isPending) {
    console.time('transition');
  } else {
    console.timeEnd('transition');
  }
}, [isPending]);

3. Accessibility Considerations:

jsx
// Announce loading state to screen readers
{
  isPending && (
    <div
      role='status'
      aria-live='polite'
    >
      Loading new content...
    </div>
  );
}

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

Junior Level:

  • Q: useTransition làm gì?
  • A: Cho phép mark state updates là non-urgent, React có thể interrupt để handle urgent updates trước, giúp UI responsive hơn.

Mid Level:

  • Q: Khi nào nên dùng useTransition vs useMemo?
  • A: useMemo cache computation results, useTransition defer rendering. Dùng useMemo khi muốn prevent re-computation, dùng useTransition khi computation must run nhưng rendering có thể defer. Thường combine cả 2: useMemo để optimize calculation, useTransition để defer rendering.

Senior Level:

  • Q: Giải thích cách useTransition work internally và trade-offs của nó
  • A: useTransition leverages React's concurrent renderer để split work thành chunks, checking for higher-priority updates between chunks. Trade-offs:
    • Pros: Better perceived performance, responsive UI, built-in loading states
    • Cons: Slightly more overhead, complexity, doesn't help with actual computation time
    • Best for: UI rendering work, not for async I/O

Architect Level:

  • Q: Design strategy để optimize large data table (100K rows) với search/filter

  • A: Multi-layered approach:

    1. Virtual scrolling (react-window) - only render visible rows
    2. Memoization (useMemo) - cache filtered results
    3. useTransition - defer filtering when typing
    4. Debounce - reduce filter frequency
    5. Web Worker - offload filtering to background thread
    6. Backend pagination - limit data transferred
    7. Progressive enhancement - basic functionality without JS

    Choose based on:

    • Data size
    • Update frequency
    • Browser support requirements
    • Team expertise

War Stories

Story 1: "The Laggy Search"

Scenario: E-commerce search với 50K products lag terribly

Initial attempt: Added useTransition
Result: Still laggy! Why?

Root cause: Wrapped EVERYTHING trong transition:
startTransition(() => {
  setQuery(value);      // ❌ Input lag!
  setResults(filtered); // ✅ This should be in transition
});

Fix: Only wrap result update
setQuery(value);        // ✅ Instant input
startTransition(() => {
  setResults(filtered); // ✅ Deferred results
});

Lesson: Understand what's urgent vs non-urgent

Story 2: "The Async Confusion"

Scenario: Team used useTransition cho data fetching

Code:
startTransition(async () => {
  const data = await fetch('/api/data');
  setData(data);
});

Problem:
- isPending becomes false IMMEDIATELY
- User sees loading state flash
- Confusing UX

Fix: Manual loading state
setLoading(true);
const data = await fetch('/api/data');
setData(data);
setLoading(false);

Lesson: useTransition cho UI work, not I/O work

Story 3: "The Double Transition"

Scenario: Dashboard với multiple filters, used separate transitions

const [isPending1, startTransition1] = useTransition();
const [isPending2, startTransition2] = useTransition();

Problem: Results didn't match filters (race conditions)

Fix: Single transition, single source of truth
const [isPending, startTransition] = useTransition();

const updateFilters = (newFilters) => {
  startTransition(() => {
    setResults(applyFilters(data, newFilters));
  });
};

Lesson: Keep transitions simple, coordinate updates

🎯 PREVIEW NGÀY MAI

Ngày 48: useDeferredValue - Deferred State

Tomorrow sẽ học:

  • useDeferredValue hook - alternative to useTransition
  • When to use useDeferredValue vs useTransition
  • Throttling renders with deferred values
  • Combining useDeferredValue với memo

Prepare:

  • Review useTransition concepts
  • Nghĩ về scenarios: "defer state" vs "defer update"
  • So sánh useTransition patterns hôm nay

Hint: useDeferredValue is declarative (value-based), useTransition is imperative (action-based). Which one more intuitive?

See you tomorrow! 🚀

Personal tech knowledge base