React Js

關於useTransition

Created: 2024/12/12
Updated: 2024/12/12

紀錄react 18中關於useTransition


useTransition 這個新的hook在React 18引入,主要用來幫助管理可能阻塞 UI 的狀態更新。允許將某些更新標記為"過渡",這些更新可以被更緊急的更新所打斷。

這可以通過使 UI 更加響應來改善用戶體驗,即使在處理緩慢或複雜的狀態更新時。

useTransition 返回一個包含兩個元素的陣列:

  1. isPending:一個布林值,表示當前是否有過渡正在進行中。
  2. startTransition:一個函數,用來包裝應被視為過渡的狀態更新。
const [isPending, startTransition] = useTransition();

基本用法和例子

當你希望在不阻塞 UI 的情況下更新狀態時,可以將狀態更新包裝在 startTransition 函數內。以下是一個基本例子:

import React, { useState, useTransition } from 'react';
import './App.css';
 
// 渲染大量組件
function HeavyList({ query }) {
  const items = [];
  // 創建大量 DOM 元素
  for (let i = 0; i < 20000; i++) {
    if (query === '' || i.toString().includes(query)) {
      items.push(
        <div key={i}>
          Item {i} - {query}
        </div>
      );
    }
  }
  return <div>{items}</div>;
}
 
function SearchWithTransition() {
  const [isPending, startTransition] = useTransition();
  const [input, setInput] = useState('');
  const [query, setQuery] = useState('');
 
  const handleChange = (e) => {
    const value = e.target.value;
    setInput(value); // 立即更新輸入框
 
    startTransition(() => {
      setQuery(value); // 延遲更新渲染
    });
  };
 
  return (
    <div>
      <h3>使用 useTransition</h3>
      <input type="text" value={input} onChange={handleChange} />
      {isPending && <p style={{ color: 'green' }}>🔄 更新中...</p>}
      <div
        style={{
          maxHeight: '400px',
          overflow: 'auto',
          border: '1px solid #ccc',
        }}
      >
        <HeavyList query={query} />
      </div>
    </div>
  );
}
 
function SearchWithoutTransition() {
  const [input, setInput] = useState('');
 
  const handleChange = (e) => {
    const value = e.target.value;
    setInput(value); // 直接更新,會導致立即重新渲染
  };
 
  return (
    <div>
      <h3>不使用 useTransition</h3>
      <input type="text" value={input} onChange={handleChange} />
      <div
        style={{
          maxHeight: '400px',
          overflow: 'auto',
          border: '1px solid #ccc',
        }}
      >
        <HeavyList query={input} />
      </div>
    </div>
  );
}
 
function App() {
  return (
    <div>
      <h1>useTransition 對比</h1>
      <p>
        <strong>測試方法:在兩個輸入框中快速連續打字(例如:12345)</strong>
      </p>
      <div style={{ display: 'flex', gap: '20px' }}>
        <div style={{ flex: 1 }}>
          <SearchWithTransition />
        </div>
        <div style={{ flex: 1 }}>
          <SearchWithoutTransition />
        </div>
      </div>
    </div>
  );
}
 
export default App;
 

傳遞給startTransition的函數,能進行更新狀態和執行effect操作。並且不會阻塞用戶的交互。

通過將狀態更新標記為過渡,來確保更緊急的更新(如用戶輸入)優先處理。這有助於保持 UI 的響應性。

在這個例子中,輸入框中的輸入操作不會被狀態更新所阻塞(測試時,可以把devtools Performance的cpu效能調低)。相反,狀態更新將被視為過渡來處理,確保輸入保持響應。

注意事項

startTransition必須要是同步的。

錯誤示範:

// ❌ 錯誤:在 startTransition 內使用異步操作
startTransition(() => {
  setTimeout(() => {
    setState(newState);
  }, 1000);
});

正確做法:

// ✅ 正確:異步操作在外部,只將狀態更新包裝在 transition 中
setTimeout(() => {
  startTransition(() => {
    setState(newState);
  });
}, 1000);

錯誤示範:

startTransition(async () => {
  await someAsyncFunction();
  // ❌ 錯誤:在呼叫 startTransition 後更新
  setPage('/about');
});

正確做法:

await someAsyncFunction();
startTransition(() => {
  // ✅ 正確:在呼叫 startTransition 後更新
  setPage('/about');
});

原理

useTransition是將一部份的狀態更新處理標記成較低悠閒及任務。讓低優先級的過度更新延遲去做,先去做高優先級任務。

像是表單輸入、按鈕點擊這些高優先擊任務會先更新,而相對不影響相互的過渡性任務,像是大量數據渲染或者動畫,可以延遲執行。

用戶操作(如輸入)
    |
    v
React 接收狀態更新,優先級判定
    |
    |
    +---> 是否在 startTransition 內?
    |         |
    |         +---> 是 ---> 標記為低優先級
    |         +---> 否 ---> 標記為高優先級
    v
Scheduler 調度器
    |
    +---> 高優先級任務(立即執行)
    |         |
    |         +---> 更新 DOM
    |         +---> 用戶看到即時反饋
    |
    +---> 低優先級任務(延遲執行)
              |
              +---> 等待高優先級任務完成
              +---> 在瀏覽器空閒時執行
              +---> 可被新的高優先級任務中斷

關鍵要點:

  • useTransition 用於標記低優先級的狀態更新
  • 始終保持緊急更新(如輸入框)同步進行
  • 不要在 startTransition 內部直接使用異步操作
  • 使用 isPending 提供視覺反饋,改善用戶體驗
  • Suspense 配合使用可避免不必要的加載指示器