Closed2

Hooks がなぜ conditional に呼べないか理解する

nissy-devnissy-dev

React Summit での 「You Can’t Use Hooks Conditionally… or Can You?」という発表で、Hooks が fiber ツリーでどうやって管理されているかなどについてざっくりと知った。ただ英語の発表ということもあり、詳細がつかめなかったので色々調べてみる。

Hooks が conditional に使えない理由を調べると、次の記事が出てきた。

https://gist.github.com/mizchi/fa00890df2c8d1f27b9ca94b5cb8dd1d

https://zenn.dev/villa_ak99/articles/e66691328fe483

https://sbfl.net/blog/2019/02/09/react-hooks-usestate/

コンポーネント内で呼ばれる Hooks の値と更新関数は、Fiber ツリーの各ノード (memoizedState フィールド) に Linked List で保持されており、Linked List の構築はまず初回レンダリングに行われる。2回目のレンダリング時には、ノードの順番はレンダリング前のものコピーしながらノードのデータの更新のみが行われる。つまり Linked List のポインターが変わらないことを前提として、ロジックが組まれている。このため、ランタイムで Hooks が呼び出される順番や回数が変換すると保持しているデータの対応関係がおかしくなり、正しく動かなくなってしまう。

ちなみに、ここら辺のコードは ReactFiberHooks.js にある mountState や updateState に対応している。

https://github.com/facebook/react/blob/21a161fa37dce969c58ae17f67f2856d06514892/packages/react-reconciler/src/ReactFiberHooks.js

なんで Linked List を使う必要があったのかどうかについては、次のブログ記事が考察していそうだった。

https://indepth.dev/posts/1007/the-how-and-why-on-reacts-usage-of-linked-list-in-fiber-to-walk-the-components-tree

元々コンポーネントツリーをトラバースする際にはルートから再帰的に render 関数を評価するような実装になっていて、組み込みの call stack を使って同期的に処理していた。一方で、この実装は UI に関して実行されるタスクをスケジューリングしたい時に相性が悪く、再帰を使っていることもあって途中で評価を止めることなども難しい。

The problem is that, in order to use those APIs, you need a way to break rendering work into incremental units. If you rely only on the call stack, it will keep doing work until the stack is empty.

そこで再帰を使わないトラバースが可能なデータ構造として、Linked list を用いた Fiber を採用したということらしい。 Tree 構造を Linked List でどうやって表現するんだ?と思っていたら 普通の child のポインター以外に sibling と呼ばれるポインターも用意するらしい。なるほど。

Wouldn't it be great if we could customize the behavior of the call stack to optimize for rendering UIs? Wouldn't it be great if we could interrupt the call stack at will and manipulate stack frames manually?

That's the purpose of React Fiber. Fiber is reimplementation of the stack, specialized for React components. You can think of a single fiber as a virtual stack frame.

https://github.com/acdlite/react-fiber-architecture

nissy-devnissy-dev

これを理解した上で、なぜ useContext は条件分岐内で呼べるのかという話がある。

次のコードで試してみると、useContext は確かに期待通りの動作をする

https://codesandbox.io/embed/festive-sanne-6j48l3?fontsize=14&hidenavigation=1&theme=dark

コードを読んでいくと、useContext は readContext に対応してそうなのでそこを読みにいく。

https://github.com/facebook/react/blob/21a161fa37dce969c58ae17f67f2856d06514892/packages/react-reconciler/src/ReactFiberHooks.js#L3230

readContext は ReactFiberNewContext.js で実装されていて、ここが Context の実装の中心であることがわかる。

https://github.com/facebook/react/blob/21a161fa37dce969c58ae17f67f2856d06514892/packages/react-reconciler/src/ReactFiberNewContext.js

ざーっと目を通すと詳細は理解できなかったが、少なくとも fiiber ツリーにデータを持たせるのではなく、独自のstack cursor というもので Context についてはデータを管理していることがわかる。また、この stack sursor は レンダリング時の Provider の有無によって Push していそうなので、確かに useContext が conditional で呼ばれていてもその stack cursor に影響はなさそうにみえる。

https://github.com/facebook/react/blob/21a161fa37dce969c58ae17f67f2856d06514892/packages/react-reconciler/src/ReactFiberBeginWork.js#L3516

ちなみに pushProvider は ReactFiberBeginWork.js で、popProvider は ReactFiberUnwindWork.js と ReactFiberCompletedWork.js で呼ばれている。

このスクラップは2023/06/11にクローズされました