research: Suspense の動作原理を整理#3
Open
miura-188cm wants to merge 1 commit into
Open
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
AI との研究結果
AIが書いた内容
Home.jsxの<Suspense>を題材に、Suspense が何に反応する仕組みかを公式ドキュメントベースで整理した。1. Suspense の本質
公式ドキュメントの記述:
Suspense は 「Promise を監視している」のではなく「子の "suspend" という振る舞いに反応する」装置。Promise が pending であることを React が直接観測しているわけではない。
2. Suspense を起動できるのは 3 種類だけ
公式が明示している Suspense-enabled data sources:
つまり
useだけではなく、React.lazy()や Suspense 対応 framework の fetch も起動できる。逆に、素のfetch().then(...)やuseEffect内の Promise は Suspense と統合されない。3. 内部機構: Promise の throw
use(promise)やlazy()は内部で Promise を throw している だけ。コンポーネントから直接throw promiseしても全く同じ挙動になる。これが Suspense の生の動作で、
use/lazyはこの上にキャッシュと状態管理を被せた糖衣構文。4. 「定義」と「throw」は責務が完全に違う
const p = fetch(...)throw pthrow fetch(...)は「定義 + throw を 1 行で書いた」だけで、内部的には 2 ステップ。throw 自体は fetch を起こさない (起動済みの Promise を React に渡すだけ)。Promise オブジェクト自体は React のことを知らないし、React も世の中の Promise を監視しているわけではない。throw が両者を繋ぐ唯一の橋。例外との対比で整理すると綺麗:
Promise も同じで、定義は async work の起点、throw は control-flow の制御。
5. 落とし穴: キャッシュなしでは無限 suspend
正しくは Promise インスタンスを再利用 + 状態管理:
useはこの状態管理を内蔵してくれるので、アプリコードでは直接 throw する必要はない。6. transition との関係
公式ドキュメントの正確な記述:
「transition 中は fallback を出さない」は不正確で、正しくは 「既に表示されている内容を fallback で隠さない」 だけ。新規に作られた Suspense 境界は普通に fallback を出す。
Home.jsxの「初回は fallback / 更新時はそのまま」の挙動はこの仕様の帰結。7. なぜ throw Promise が選ばれたのか (設計史)
実は React チームが解きたかった問題は "algebraic effects (代数的効果) の代用品"。Sebastian Markbåge 自身が gist のタイトルを "Poor man's algebraic effects" と名付けている。
解きたかった問題:
Dan Abramov:
検討された代替案と却下理由:
async functionにするyield)throw が勝った 5 つの理由:
ErrorかPromiseかで挙動を分けるだけ。新規概念ゼロ8. throw promise から use(promise) への進化
throw 方式の欠点: 「任意の関数から suspend できてしまう」。RFC #229:
そこで
useは throw メカニズムを捨てたのではなく、throw できる場所をフックに限定する API レイヤを被せたもの。中身は同じで、規律化された API 表面を被せただけ。throw promise は internal implementation detail に降格、
useが public API。書けば動くが、推奨されない。9. SC と CC で API が違う理由
await fetchX()use(promise)CC が
async functionにできない理由:async functionは Promise を返すSC は再 render なし、線形に終わればよく、
awaitの中断は JS ランタイムが面倒を見てくれるので普通に書ける。つまり
useの存在理由は、CC が async function になれないことを補うため。10. SC → CC 境界で Promise を渡す pattern
SC で await すると render 全体がブロックされる。Promise のまま渡せば SC は即時に JSX を返せて、CC が独自の Suspense 境界で待つ。ストリーミングの粒度を細かく制御するために
useが活きる。11. Next.js loading.tsx の正体
ファイル規約による Suspense の自動配置:
役割分担:
page.tsx<Suspense>の childrenloading.tsx<Suspense fallback={...}>の fallback<Suspense>でくるむ重要な点:
"use client"でuse()で throw しても、最近接 Suspense (= loading.tsx を fallback とするやつ) が catch する12. 最近接ルール (Suspense / Error Boundary 共通)
ネストしたとき:
throw された値は call stack を unwind して 最初に出会った catch で捕まる。これは Error Boundary と完全に対称な挙動で、JS の throw/catch の挙動がそのまま React の境界に投影されている。
「優先順位」という独立したルールは存在せず、
error.tsxも含めて App Router の境界は 規約ベースのファイル配置で React primitive を自動配線しているだけ。13. 一行まとめ
主要ソース
miura が書いた内容
throw Promiseするのは非推奨今後様式が変わるかもしれない.
黙って
useを使うべきuseを 使えば,コンポーネント内部で loading とかを出す必要がなくなるし便利コンポーネントがスッキリする
積極的に使うべし