■ 1. TanStack Redact の位置づけ
- React API 対応の軽量ランタイム:
- 既存の JSX や Hooks を使ったコードを維持したまま、実行時に使われる React の実装を置き換える
- ビルドツールを通じて import 先を自身の実装へ差し替える
- API 名が同じでも動作は同一でない:
- React のすべての動作が再現されるわけではない
- 同期描画を採用し、描画を優先順位に応じて中断・再開する仕組みを省いている
- この違いは useDeferredValue や startTransition の動作に影響する
■ 2. Preact との方針の違い
- Preact の互換レイヤー:
- Preact 自体は React の再実装を目的としていない
- preact/compat という互換レイヤーを通じて React のコードやライブラリを利用できるようにしている
- 作者が preact/compat で直面した課題:
- 作者 Tanner Linsley 氏は Projecting React で、最初は preact/compat の導入を試したと振り返っている
- use() の挙動、React 19 の Server Actions 関連 API、Portal、Error Boundary、hydration の細部で互換性の問題が重なった
- 追加の互換処理が増えていった
- Redact が選んだ方針:
- React の公開 API を出発点に、TanStack Start で必要とする範囲へ絞った実装を作る
- 互換レイヤーを重ねるのではなく、必要な React の API と動作から実装を組み立てる
■ 3. Vite への導入
- 導入手順:
- @tanstack/redact パッケージを --save-exact 付きでインストールする
- Vite のプラグインに redact() を追加する
- JSX 変換の設定:
- Vite 内蔵の JSX 変換を使う場合は esbuild: { jsx: 'automatic' } を明示する
- React の Vite プラグインを使う場合は automatic JSX runtime がデフォルトで有効なため明示は不要
- 差し替えの対象:
- react は @tanstack/redact、react-dom/client は @tanstack/redact/dom-client へ解決される
- JSX を実行するための react/jsx-runtime なども同様に置き換わる
- RSC 環境の扱い:
- プラグインはクライアントと SSR のビルドを扱う
- React Server Components の環境では import を差し替えず、本家 React がそのまま使われる
■ 4. React が置く制約と concurrent rendering
- コンポーネントの純粋性:
- 同じ props・state・context に対して同じ結果を返し、レンダリング中には外部の状態を変更しない
- 副作用はイベントハンドラーや Effect など、レンダリングとは別の場所で実行する
- この制約により、コンポーネントをいつ評価するかを制御しやすくなる
- concurrent rendering の目的:
- ユーザーの操作に対して応答性を維持する
- 検索欄への入力に合わせて巨大なリストを更新する画面では、一覧の描画完了まで待たせると入力が重く感じられる
- 描画の途中で中断し、より優先度の高い更新を先に処理できる
- 優先順位を伝える API:
- startTransition はコールバック関数内で指定した状態更新を、ほかの更新を妨げない Transition として扱う
- useDeferredValue は値の反映を遅らせ、その値を使う UI の更新を後から試みる
- 商品一覧での使用例:
- 入力欄の値 text と、一覧の絞り込みに使う値 query を分ける
- 入力欄は通常の状態更新で反映し、一覧の更新だけを startTransition で囲む
- ProductList は挙動を観察しやすいよう、5,000 件の商品の各行で意図的に重い計算をする
- 一覧の描画中に次の入力が届くと描画を中断して入力欄を優先し、最新の検索語で描画をやり直す
■ 5. Redact が選んだ同期描画
- concurrent scheduling の不採用:
- React の API と日常的な動作を提供しながら、描画する仕事の優先順位や実行タイミングの管理を持たない
- 初期版 0.0.1 のサイズ分析には、中断可能な描画などを省いて実装を小さくする方針が記されている
- PR #24 での方針維持:
- 2026 年 9 月 11 日にマージされた PR #24 で、React の API や DOM・SSR の互換性を拡張している
- この変更でも同期描画と、Action・楽観的更新に関する Hooks の動作省略を維持する方針が明記されている
- 設計の要点:
- React のコンポーネントモデルを引き継ぎつつ、実行時の責務を絞る
- 純粋なコンポーネントを前提にしつつ、ランタイムが引き受ける処理を減らす
■ 6. 中断・再開を支える実装の省略
- 中断可能な描画に必要な処理:
- 更新の優先順位を管理し、どこまで描画したかを保持し、処理を譲るタイミングを判断する
- 新しい入力が届いた場合は、途中の描画をやり直す処理も必要になる
- React の描画処理には workInProgress、lane、shouldYield が登場し、これらを管理する JavaScript もランタイムとして配信される
- Redact が省略・簡略化する処理:
- 更新の選択では、React の lane に基づく優先順位管理を実装しない
- 描画の進行では、途中で処理を譲る仕組みを実装しない
- 値の遅延では、useDeferredValue が渡された値をそのまま返す
- useDeferredValue の実装:
- 受け取った値をそのまま返すだけで、以前の値を保持して別の優先順位で描画する処理はない
■ 7. 同期描画以外の軽量化
- イベント処理の簡略化:
- 要素へ addEventListener でハンドラーを登録し、ネイティブイベントに nativeEvent などの互換性を補う
- React の合成イベントの仕組み全体を再現することは避けている
- 機能フラグによる除外:
- 機能フラグを無効にすると、Vite プラグインが対象機能の import 先を簡略化した実装へ差し替える
- ブラウザに本来の実装を配信してから実行を止める方式ではなく、ビルド時に実装を除外する方式を採る
- nano プリセット:
- オプション機能をまとめて無効にし、必要な機能だけを選ぶ設定
- 無効化時の挙動:
- context を無効にすると Provider の値は伝わらない
- hydration を無効にすると hydrateRoot は例外を投げる
■ 8. React と Redact の比較
- 置き換えの手軽さ:
- コードはそのままで、Vite のプラグインを有効にするだけで React の import が Redact の実装へ置き換わる
- 挙動の違い:
- startTransition を呼んでも、使わなかった場合と同じく入力欄と一覧の更新が同じ優先順位で扱われる
- デモでは入力欄への入力が遅延する
- ビルドサイズ:
- 商品一覧を Vite で production build した JavaScript 全体の gzip サイズは、React が 69,714 bytes
- 同条件で Redact の full プリセットは 20,112 bytes となり、React より小さくなる