Open112

Reactの内部実装を見る(2. mountStateの中身)

Yug (やぐ)Yug (やぐ)

このmountStateを見ていく

packages\react-reconciler\src\ReactFiberHooks.js
const HooksDispatcherOnMount: Dispatcher = {
  readContext,

  // ...
  useState: mountState,
  // ...
};
Yug (やぐ)Yug (やぐ)

同ファイルに定義元がある

ReactFiberHooks.js
function mountState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  const hook = mountStateImpl(initialState);
  const queue = hook.queue;
  const dispatch: Dispatch<BasicStateAction<S>> = (dispatchSetState.bind(
    null,
    currentlyRenderingFiber,
    queue,
  ): any);
  queue.dispatch = dispatch;
  return [hook.memoizedState, dispatch];
}

引数と返り値の型は見慣れたものなのでok

実際の処理を1行1行見ていくか。まずこれ

ReactFiberHooks.js
const hook = mountStateImpl(initialState);

mountStateImplってなんだ?見ていこう

Yug (やぐ)Yug (やぐ)

同ファイルにある。

ReactFiberHooks.js
function mountStateImpl<S>(initialState: (() => S) | S): Hook {
  const hook = mountWorkInProgressHook();
  if (typeof initialState === 'function') {
    const initialStateInitializer = initialState;
    // $FlowFixMe[incompatible-use]: Flow doesn't like mixed types
    initialState = initialStateInitializer();
    if (shouldDoubleInvokeUserFnsInHooksDEV) {
      setIsStrictModeForDevtools(true);
      try {
        // $FlowFixMe[incompatible-use]: Flow doesn't like mixed types
        initialStateInitializer();
      } finally {
        setIsStrictModeForDevtools(false);
      }
    }
  }
  hook.memoizedState = hook.baseState = initialState;
  const queue: UpdateQueue<S, BasicStateAction<S>> = {
    pending: null,
    lanes: NoLanes,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),
  };
  hook.queue = queue;
  return hook;
}

引数はmountStateと同じでinitialState。だが返り値はHook。

Yug (やぐ)Yug (やぐ)

Hookの型を見てみるとこれ

ReactFiberHooks.js
export type Hook = {
  memoizedState: any,
  baseState: any,
  baseQueue: Update<any, any> | null,
  queue: any,
  next: Hook | null,
};

memoizedStateはuseStateのstateそのものだろう。

baseStateってなんだ?分からんので飛ばす。

baseQueueはUpdateという型を使ってるな。これを見てみよう

Yug (やぐ)Yug (やぐ)

あぁ、あのupdateオブジェクトか
(revertLaneっていうプロパティは初見だ、新しくできた?)

ReactFiberHooks.js
export type Update<S, A> = {
  lane: Lane,
  revertLane: Lane,
  action: A,
  hasEagerState: boolean,
  eagerState: S | null,
  next: Update<S, A>,
};

WIPのFiberツリー(仮想DOM②)への更新内容そのもの。循環リストになってる。
でupdateはFiber.memoizedStateに格納されてる(この記事によると)
https://zenn.dev/ktmouk/articles/68fefedb5fcbdc#1.-フックから循環リストを作成

Yug (やぐ)Yug (やぐ)

んで次、queueというのは何だ?

これか?flushWork関数(タスクを実行する関数)が積まれてるマクロタスク/マイクロタスクのことかも

タスクを取り出して処理するflushWorkという関数を「キュー」に積んで遅延実行されます。ここでいう「キュー」というのは、Reactの独自実装ではありません。ブラウザのJavaScriptが元々持っている「マクロタスク」もしくは「マイクロタスク」という概念です。

https://zenn.dev/ktmouk/articles/68fefedb5fcbdc#2.タスクを処理する関数をマクロタスクとして追加


最後、nextというプロパティ。Hookが入ってる。

ということはHookも連結リストになってそうだな。
そういえばフックも循環リストだとcallocさんから聞いたことがあるな
https://x.com/calloc134/status/1861254365543309792

Yug (やぐ)Yug (やぐ)

mountStateImplの引数と返り値はわかったので処理を見ていこう

まずこれ

ReactFiberHooks.js
const hook = mountWorkInProgressHook();

今度はmountWorkInProgressHookとかいう知らん関数使われてる

これも見ていくか

Yug (やぐ)Yug (やぐ)

これ

ReactFiberHooks.js
function mountWorkInProgressHook(): Hook {
  const hook: Hook = {
    memoizedState: null,

    baseState: null,
    baseQueue: null,
    queue: null,

    next: null,
  };

  if (workInProgressHook === null) {
    // This is the first hook in the list
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    // Append to the end of the list
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}

workInProgressHookというのは何だ?WIPのフック、現在作業中のフック...

まぁでも確かにフックを順番に見ていくという記述はこの記事にもあったので、今作業中のフックというのはそのままの意味か。
https://gist.github.com/mizchi/fa00890df2c8d1f27b9ca94b5cb8dd1d

workInProgressHookがnullだったらこれやってる

ReactFiberHooks.js
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;

hookは今作ったばっかの、全部のプロパティの値がnullのフックオブジェクト。

それをworkInProgressHook(現在作業中のフック)に代入。
この時点でworkInProgressHookはnullでなくなる(プロパティの値は全部nullだけど)

で、そのworkInProgressHookをcurrentlyRenderingFiber.memoizedStateに代入。

currentlyRenderingFiberは現在作業中のfiberだろう。そのmemoizedStateプロパティにworkInProgressHookを代入してる

Yug (やぐ)Yug (やぐ)

...んんん?てことはFiberという型のmemoizedStateというプロパティは対応するフックを格納しているっぽいな、、!

この記事のこの記述から、Fiber.memoizedStateはupdateオブジェクトを循環リストとして格納してるもんだと勘違いしてた

この循環リストをフックの持ち主であるFiberのmemoizedStateに保存しておきます。

https://zenn.dev/ktmouk/articles/68fefedb5fcbdc#1.-フックから循環リストを作成

この記述は間違いということか?...とりあえずそれで理解しよう
(循環リストとして保存するのはFiber.memoizedStateではなくFiber.updateQueueっぽいしなぁ)

であればこの記事のこの記述も納得がいくな。なるほど、Fiberとフック(フックス?)は対応してるのか

// Hooks are stored as a linked list on the fiber's memoizedState field.
hooksはfiberにLinkedListとして格納されるようです。実はuseStateには「毎回同じ順番で同じ回数呼び出さないとデータがずれる」という仕様があるのですが、管理が単なるLinkedListだからですね。

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

フックが単数で入ってるのかフックス(複数)なのか分からんけど。今回のコードだと1つのフックをただ代入してるだけなのでフックか?

と思ったけどこの記事ではHooksとかlistとか言われてるんだよなぁ...

あーでもこのコメント部分を読んだらリストになってるのがmemoizedStateだという意味っぽいぞ

ReactFiberHooks.js
// This is the first hook in the list
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;

じゃあ多分Fiber.memoizedStateはフックであって対応するフックが1つ以上入るリストだな

Yug (やぐ)Yug (やぐ)

とりあえずworkInProgressHook(現在作業中のフック)をそのcurrentlyRenderingFiber.memoizedStateに代入してるということ。FiberとHookを対応させてる。

ReactFiberHooks.js
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
Yug (やぐ)Yug (やぐ)

このifはファイル内で見つけた最初のuseState、みたいなことか。だからnull

if (workInProgressHook === null) {
    // This is the first hook in the list
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
}
Yug (やぐ)Yug (やぐ)

で、次else

ReactFiberHooks.js
} else {
    // Append to the end of the list
    workInProgressHook = workInProgressHook.next = hook;
}

どういうことだぁぁ...??

なんかhooksの型やfiber.memoizedStateなどがよくわかんなくて混乱してきた...


GPTと問答したらめっちゃなるほどぉぉになったので一応メモ

Fiber.memoizedStateはフックを1つ格納してる。

だがフック自体は単方向リストで次のフックへの参照をHook.nextで保持している

だからFiber.memoizedStateは実質複数のフックを保持していると言えるよね~みたいな話。

なるほどぉぉぉ

こんなイメージ

memo.jsx
function MyComponent() {
  const [count, setCount] = useState(0);         // フック1
  const [name, setName] = useState("React");     // フック2
  const [isActive, setIsActive] = useState(false); // フック3
  return <div>{count} {name} {isActive ? "Active" : "Inactive"}</div>;
}
memo.js
Fiber.memoizedState = {
  memoizedState: 0,      // フック1の現在の状態
  baseState: 0,
  queue: { ... },        // フック1の更新キュー
  next: {                // 次のフック(フック2)
    memoizedState: "React",
    baseState: "React",
    queue: { ... },
    next: {              // 次のフック(フック3)
      memoizedState: false,
      baseState: false,
      queue: { ... },
      next: null         // 最後のフック
    }
  }
}

なのでmemoizedStateが直接的に保持しているもの、というか先頭?のフックは一番最初のフックということだな

Fiber.memoizedStateはそんな感じ。つまりただのフックの連結リスト。
(でもそれならmemoizedStateなんて名前じゃなくてhooksListとかにすれば良いのに謎だな)

んでHooks.memoizedStateはただのuseStateのstateそのもの。countの値とかそういうやつ。

これを踏まえて読み直してみよう

Yug (やぐ)Yug (やぐ)
ReactFiberHooks.js
function mountWorkInProgressHook(): Hook {
  const hook: Hook = {
    memoizedState: null,

    baseState: null,
    baseQueue: null,
    queue: null,

    next: null,
  };

  if (workInProgressHook === null) {
    // This is the first hook in the list
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    // Append to the end of the list
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}
Yug (やぐ)Yug (やぐ)
  • workInProgressHookがnullだったら(=はじめてのuseStateフックだったら)

プロパティが全部nullのhookをworkInProgressHookに代入(初期化)する
->workInProgressHookをcurrentlyRenderingFiber.memoizedStateに代入する
= memoizedStateリスト内の最初のフックということになる

// This is the first hook in the list


  • workInProgressHookがnullでなかったら(=2回目以降のuseStateフックだったら)

プロパティが全部nullのhookをworkInProgressHook.nextに代入する
(ここまではifと一緒)
->workInProgressHook.nextをworkInProgressHooktに代入する

うーんとこれはどういうことだ?謎すぎる

workInProgressHook.nextに空hookを代入しても、そのあとworkInProgressHooktに空hook代入しちゃうのでnextへの代入が完全に意味無くなるぞ。何の意味があるんだこれ?

つまりこれだと何をしようが最終的にworkInProgressHook.nextはnullになってしまうはずだぞ。
自分の環境でも模擬的に試したらやはりそうだった。nextへの代入は意味無いはず。

うーん、ということで実質これをやってるだけのはずだぞ

ReactFiberHooks.js
workInProgressHook = hook;

つまりすでにcurrentlyRenderingFiber.memoizedStateはあるので、そこへの代入は必要なく、ただこれから作業するという意味でworkInProgressHookにhookを代入(初期化)した感じやな

謎なのでつぶやいといた
https://x.com/clumsy_ug/status/1865744358382911682

Yug (やぐ)Yug (やぐ)

んで最後に、ifでもelseでも関係なくworkInProgressHookを返す

ReactFiberHooks.js
return workInProgressHook;

以上でmountWorkInProgressHook関数はok。

ReactFiberHooks.js
function mountWorkInProgressHook(): Hook {
  const hook: Hook = {
    memoizedState: null,

    baseState: null,
    baseQueue: null,
    queue: null,

    next: null,
  };

  if (workInProgressHook === null) {
    // This is the first hook in the list
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    // Append to the end of the list
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}

呼び出し元のmountStateImpl関数に戻る

Yug (やぐ)Yug (やぐ)

戻ってきた

ReactFiberHooks.js
function mountStateImpl<S>(initialState: (() => S) | S): Hook {
  const hook = mountWorkInProgressHook();
  if (typeof initialState === 'function') {
    const initialStateInitializer = initialState;
    // $FlowFixMe[incompatible-use]: Flow doesn't like mixed types
    initialState = initialStateInitializer();
    if (shouldDoubleInvokeUserFnsInHooksDEV) {
      setIsStrictModeForDevtools(true);
      try {
        // $FlowFixMe[incompatible-use]: Flow doesn't like mixed types
        initialStateInitializer();
      } finally {
        setIsStrictModeForDevtools(false);
      }
    }
  }
  hook.memoizedState = hook.baseState = initialState;
  const queue: UpdateQueue<S, BasicStateAction<S>> = {
    pending: null,
    lanes: NoLanes,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),
  };
  hook.queue = queue;
  return hook;
}
Yug (やぐ)Yug (やぐ)

1行目のこれは、プロパティが全部nullの初期化されたworkInProgressHook。

ReactFiberHooks.js
const hook = mountWorkInProgressHook();
Yug (やぐ)Yug (やぐ)

useStateの初期値として受け取ったものが関数であれば、まずその関数をinitialStateInitializerという変数で保存

ReactFiberHooks.js
if (typeof initialState === 'function') {
    const initialStateInitializer = initialState;

関数ということはこれはuseStateのinitializer function(初期化関数)のことだな
https://ja.react.dev/reference/react/useState#parameters

Yug (やぐ)Yug (やぐ)

でその実行結果をinitialStateに代入。initailStateが関数から値になったということ。

ReactFiberHooks.js
// $FlowFixMe[incompatible-use]: Flow doesn't like mixed types
initialState = initialStateInitializer();

んでコメントがあるが、多分flowの型チェックを無視するのではないだろか

initialStateは最初関数だったのにただの値に変わっちゃうのは確かに型エラーが出そうなのでそれを防ぐために。

Reactのリンタ抑制に似てる。// eslint-ignore-next-line react-hooks/exhaustive-depsってやつ
https://ja.react.dev/learn/lifecycle-of-reactive-effects#what-to-do-when-you-dont-want-to-re-synchronize

Yug (やぐ)Yug (やぐ)

次のスコープ

ReactFiberHooks.js
if (shouldDoubleInvokeUserFnsInHooksDEV) {
  setIsStrictModeForDevtools(true);
  try {
    // $FlowFixMe[incompatible-use]: Flow doesn't like mixed types
    initialStateInitializer();
  } finally {
    setIsStrictModeForDevtools(false);
  }
}

shouldDoubleInvokeUserFnsInHooksDEVって何だ?

Yug (やぐ)Yug (やぐ)

それがtrueだったらsetIsStrictModeForDevtools(true);という処理をしていることから、strict modeをtrueにすべきかどうかの情報が入っているはず

直訳しても、「フックス開発環境(?)において、ユーザーの関数を二度呼び出すべきかどうか」なのでStrict Mode関連だろうな

じゃあそのsetIsStrictModeForDevtoolsとは?

これ

packages\react-reconciler\src\ReactFiberDevToolsHook.js
export function setIsStrictModeForDevtools(newIsStrictMode: boolean) {
  if (consoleManagedByDevToolsDuringStrictMode) {
    if (typeof log === 'function') {
      // We're in a test because Scheduler.log only exists
      // in SchedulerMock. To reduce the noise in strict mode tests,
      // suppress warnings and disable scheduler yielding during the double render
      unstable_setDisableYieldValue(newIsStrictMode);
      setSuppressWarning(newIsStrictMode);
    }

    if (injectedHook && typeof injectedHook.setStrictMode === 'function') {
      try {
        injectedHook.setStrictMode(rendererID, newIsStrictMode);
      } catch (err) {
        if (__DEV__) {
          if (!hasLoggedError) {
            hasLoggedError = true;
            console.error(
              'React instrumentation encountered an error: %s',
              err,
            );
          }
        }
      }
    }
  } else {
    if (newIsStrictMode) {
      disableLogs();
    } else {
      reenableLogs();
    }
  }
}

1個1個見ていくか

Yug (やぐ)Yug (やぐ)

まずconsoleManagedByDevToolsDuringStrictMode

「ストリクトモード時にDevToolsが管理するコンソール」???

定義元はこれ

packages\shared\ReactFeatureFlags.js
export const consoleManagedByDevToolsDuringStrictMode = true;

んでReactFiberDevToolsHook.jsファイル内を見るとconsoleManagedByDevToolsDuringStrictModeに何かしらを代入している式は1つも無いので、true確定

本番環境ではfalseを代入するみたいな処理が他であるんじゃないかと予想

とりあえずdevモードならtrueになるみたいな雑なイメージでいく

Yug (やぐ)Yug (やぐ)

なので今回のif文は通過確定で、その中のスコープはこれ

ReactFiberDevToolsHook.js
if (typeof log === 'function') {
  // We're in a test because Scheduler.log only exists
  // in SchedulerMock. To reduce the noise in strict mode tests,
  // suppress warnings and disable scheduler yielding during the double render
  unstable_setDisableYieldValue(newIsStrictMode);
  setSuppressWarning(newIsStrictMode);
}

logってなんや?

Yug (やぐ)Yug (やぐ)

これ

packages\react-reconciler\src\Scheduler.js
// this doesn't actually exist on the scheduler, but it *does*
// on scheduler/unstable_mock, which we'll need for internal testing
export const log = Scheduler.log;

これは実際にはスケジューラー上には存在しないが、内部テストに必要なscheduler/unstable_mock上には存在する

Schedulerとは何だろう

Yug (やぐ)Yug (やぐ)

こうimportされてる

Scheduler.js
// This module only exists as an ESM wrapper around the external CommonJS
// Scheduler dependency. Notice that we're intentionally not using named imports
// because Rollup would use dynamic dispatch for CommonJS interop named imports.
// When we switch to ESM, we can delete this module.
import * as Scheduler from 'scheduler';

このモジュールは、外部のCommonJS Scheduler依存性のESModulesラッパーとしてのみ存在します。Rollupは、CommonJS相互接続の名前付きインポートに対してダイナミック・ディスパッチを使用するので、意図的に名前付きインポートを使用していないことに注意してください。ESModulesに切り替えたら、このモジュールを削除できます。

Yug (やぐ)Yug (やぐ)

なるほど、なんとなく理解。
CommonJSのESMラッパーがSchedulerか

Scheduler.js
import * as Scheduler from 'scheduler';

んでそのimport先まで更に辿ってみると、ファイルの中身はこれだけ

packages\scheduler\index.js
'use strict';
export * from './src/forks/Scheduler';

さらにたらい回しか。その先のファイルのexportしてる内容を見にいくとこの4か所

packages\scheduler\src\forks\Scheduler.js
export type Callback = boolean => ?Callback;

export opaque type Task = {
  id: number,
  callback: Callback | null,
  priorityLevel: PriorityLevel,
  startTime: number,
  expirationTime: number,
  sortIndex: number,
  isQueued?: boolean,
};

export {
  ImmediatePriority as unstable_ImmediatePriority,
  UserBlockingPriority as unstable_UserBlockingPriority,
  NormalPriority as unstable_NormalPriority,
  IdlePriority as unstable_IdlePriority,
  LowPriority as unstable_LowPriority,
  unstable_runWithPriority,
  unstable_next,
  unstable_scheduleCallback,
  unstable_cancelCallback,
  unstable_wrapCallback,
  unstable_getCurrentPriorityLevel,
  shouldYieldToHost as unstable_shouldYield,
  requestPaint as unstable_requestPaint,
  unstable_continueExecution,
  unstable_pauseExecution,
  unstable_getFirstCallbackNode,
  getCurrentTime as unstable_now,
  forceFrameRate as unstable_forceFrameRate,
};

export const unstable_Profiling: {
  startLoggingProfilingEvents(): void,
  stopLoggingProfilingEvents(): ArrayBuffer | null,
} | null = enableProfiling
  ? {
      startLoggingProfilingEvents,
      stopLoggingProfilingEvents,
    }
  : null;

これらが全部、最終的にはScheduleとしてexportされてるということのはず

あとopaque typeとかいうflow独自の型が使われてるな。定義されたモジュール内部では普通に使えるけど外部には具体的な型の内容は隠蔽できるみたいなやつらしい。
https://flow.org/en/docs/types/opaque-types/

Yug (やぐ)Yug (やぐ)

ん?でもそれだとlogというプロパティは見当たらないのでこれがおかしいな...?

packages\react-reconciler\src\Scheduler.js
// this doesn't actually exist on the scheduler, but it *does*
// on scheduler/unstable_mock, which we'll need for internal testing
export const log = Scheduler.log;
Yug (やぐ)Yug (やぐ)

あ、コメントにヒントがあるかも

Scheduler.js
// this doesn't actually exist on the scheduler, but it *does*
// on scheduler/unstable_mock, which we'll need for internal testing

実際にscheduler上には存在しないが、scheduler/unstable_mockには存在するで、みたいなこと書かれてる

そこ見てみるか

Yug (やぐ)Yug (やぐ)

これだけ

packages\scheduler\unstable_mock.js
'use strict';

export * from './src/forks/SchedulerMock';

そのexport元を見に行くとこれ

Yug (やぐ)Yug (やぐ)

あったぞ!おそらくこれだ

function log(value: mixed): void {
  // eslint-disable-next-line react-internal/no-production-logging
  if (console.log.name === 'disabledLog' || disableYieldValue) {
    // If console.log has been patched, we assume we're in render
    // replaying and we ignore any values yielding in the second pass.
    return;
  }
  if (yieldedValues === null) {
    yieldedValues = [value];
  } else {
    yieldedValues.push(value);
  }
}

export {
  ...
  log

1行1行見ていくか

Yug (やぐ)Yug (やぐ)

console.log.nameってなんだ?

if (console.log.name === 'disabledLog' || disableYieldValue) {
  // If console.log has been patched, we assume we're in render
  // replaying and we ignore any values yielding in the second pass.
  return;
}
Yug (やぐ)Yug (やぐ)

JSでconsole.log.nameと書いてnameの定義元飛んでみたら、こうなってた

lib.es2015.core.d.ts
interface Function {
    /**
     * Returns the name of the function. Function names are read-only and can not be changed.
     */
    readonly name: string;
}

なるほど、関数名のことっぽい

例えば以下のようになる

console.log(console.log.name);  // 出力: log

function fire() {
  console.log('aiueo');
}
console.log(fire.name);  // 出力: fire

console.logのlogというのは関数であって当然その関数名もlogであるということやな

Yug (やぐ)Yug (やぐ)

ただそれで言うと、console.log.name === 'disabledLog'というのはなんだ?絶対logになるのではないのか?

if (console.log.name === 'disabledLog' || disableYieldValue) {
  // If console.log has been patched, we assume we're in render
  // replaying and we ignore any values yielding in the second pass.
  return;
}

うーん、どこかで'disableLog'に変えられる処理があるのか?

...いや、無かった

https://github.com/search?q=repo%3Afacebook%2Freact+console.log.name+%3D&type=code

じゃあもっと低レイヤーで自動的に切り替えられるみたいな話なのかもしれない。

とにかく、console.log.name === 'disabledLog' になっていたらreturnで強制終了している

それか、disableYieldValueがtruthyであってもreturnで強制終了

disableYieldValueというのは何だ?

「valueを生めない(収穫できない)」かぁ...わからん

Yug (やぐ)Yug (やぐ)

同ファイルに定義があるが、falseが代入されてるだけ

SchedulerMock.js
var disableYieldValue = false;

更新用関数もあるな

SchedulerMock.js
function setDisableYieldValue(newValue: boolean) {
  disableYieldValue = newValue;
}

直接disableYieldValue =という感じで代入されてるとこは無さそうなので、おそらくこの更新用関数で更新されてるはず

んでこの更新用関数は違う名前でexportされてる

SchedulerMock.js
export {
  ...
  setDisableYieldValue as unstable_setDisableYieldValue,

なのでunstable_setDisableYieldValueが使われてる場所を探してみよう

Yug (やぐ)Yug (やぐ)

あった。これだ

packages\react-reconciler\src\ReactFiberDevToolsHook.js
export function setIsStrictModeForDevtools(newIsStrictMode: boolean) {
  if (consoleManagedByDevToolsDuringStrictMode) {
    if (typeof log === 'function') {
      // We're in a test because Scheduler.log only exists
      // in SchedulerMock. To reduce the noise in strict mode tests,
      // suppress warnings and disable scheduler yielding during the double render
      unstable_setDisableYieldValue(newIsStrictMode);
      setSuppressWarning(newIsStrictMode);
    }

ん、まさかの偶然setIsStrictModeForDevtools関数に戻ってきた
ぐるぐる回った結果同じとこに戻ってきたのおもろい

logを発見
↓
調べた結果logを探し当てる
↓
logの中身を見てるとdisableYieldValueを発見
↓
その中身を探してたらlogを発見した場所に戻ってきた

つまり相互作用しあってる感じなんだろうな

まぁとにかくnewIsStrictModeが実質的な値であることがわかる。

newIsStrictModeはsetIsStrictModeForDevtoolsの引数なので、setIsStrictModeForDevtoolsがどのように使われてるのか探せば良い

Yug (やぐ)Yug (やぐ)

結構使われてるけど、まぁ予想できるように、開発中はtrueにすることで2回レンダーするよ、みたいなことをする必要があるが、そのtrueの部分

packages\react-reconciler\src\ReactFiberHooks.js
if (shouldDoubleRenderDEV) {
  // In development, components are invoked twice to help detect side effects.
  setIsStrictModeForDevtools(true);

なのでdisableYieldValueは実質、「strictModeかどうか≒レンダー2回すべきかどうか」みたいに雑に捕えよう
(でも変数名からはまったく想像できないものになってしまうのでちょっと謎だが...。まぁいい)

理解したので戻るか

Yug (やぐ)Yug (やぐ)

log関数の続きから

packages\scheduler\src\forks\SchedulerMock.js
function log(value: mixed): void {
  // eslint-disable-next-line react-internal/no-production-logging
  if (console.log.name === 'disabledLog' || disableYieldValue) {
    // If console.log has been patched, we assume we're in render
    // replaying and we ignore any values yielding in the second pass.
    return;
  }
  if (yieldedValues === null) {
    yieldedValues = [value];
  } else {
    yieldedValues.push(value);
  }
}

console.log.nameがdisableLogに変えられていたら、もしくはdisableYieldValueがtrueつまりstrictModeだったら(?)、returnで強制終了

んでコメントにこう書かれてる

console.logにパッチが適用されている場合は、レンダリング再生中であるとみなし、2回目のパスで得られた値はすべて無視する

ほー、strict modeによる2回目のrenderだったらconsole.log.nameがdisableLogにパッチされているということかも。その場合は「レンダリング再生中」とみなして何もしないという感じか?

Yug (やぐ)Yug (やぐ)

んで最後ここ

packages\scheduler\src\forks\SchedulerMock.js
if (yieldedValues === null) {
  yieldedValues = [value];
} else {
  yieldedValues.push(value);
}

yieldedValuesは同ファイルでこう定義されてる。まぁyieldValueが入った配列だろうな

SchedulerMock.js
let yieldedValues: Array<mixed> | null = null;

つまり引数であるvalueを配列内に入れてる(何のためかは知らん)

log関数終了。setIsStrictModeForDevtoolsに戻る

Yug (やぐ)Yug (やぐ)
packages\react-reconciler\src\ReactFiberDevToolsHook.js
export function setIsStrictModeForDevtools(newIsStrictMode: boolean) {
  if (consoleManagedByDevToolsDuringStrictMode) {
    if (typeof log === 'function') {
      // We're in a test because Scheduler.log only exists
      // in SchedulerMock. To reduce the noise in strict mode tests,
      // suppress warnings and disable scheduler yielding during the double render
      unstable_setDisableYieldValue(newIsStrictMode);
      setSuppressWarning(newIsStrictMode);
    }

    if (injectedHook && typeof injectedHook.setStrictMode === 'function') {
      try {
        injectedHook.setStrictMode(rendererID, newIsStrictMode);
      } catch (err) {
        if (__DEV__) {
          if (!hasLoggedError) {
            hasLoggedError = true;
            console.error(
              'React instrumentation encountered an error: %s',
              err,
            );
          }
        }
      }
    }
  } else {
    if (newIsStrictMode) {
      disableLogs();
    } else {
      reenableLogs();
    }
  }
}

logの型がfunctionだったら通過

コメント直訳するとこれ

Scheduler.logはSchedulerMockの中にしか存在しないので、テストの中にいる。ストリクト・モードのテストでノイズを減らすには、警告を抑制し、ダブル・レンダリング中のスケジューラの降伏を無効にする。

mockはテストだと考えて良さそう。それ以外は意味わからん。

Yug (やぐ)Yug (やぐ)

んでこれ

unstable_setDisableYieldValue(newIsStrictMode);

さっき見た通りこれは実質setDisableYieldValueで、中身はこれ

SchedulerMock.js
function setDisableYieldValue(newValue: boolean) {
  disableYieldValue = newValue;
}

つまり引数をdisableYieldValueというstrict modeかどうかみたいな謎の変数に代入するだけ

Yug (やぐ)Yug (やぐ)

んで次これ。ここからがまだ見てない。

setSuppressWarning(newIsStrictMode);

定義元はこれ

packages\shared\consoleWithStackDev.js
// We expect that our Rollup, Jest, and Flow configurations
// always shim this module with the corresponding environment
// (either rn or www).
//
// We should never resolve to this file, but it exists to make
// sure that if we *do* accidentally break the configuration,
// the failure isn't silent.

export function setSuppressWarning() {
  // TODO: Delete this and error when even importing this module.
}

意味無さそうやな。とばす

Yug (やぐ)Yug (やぐ)

setIsStrictModeForDevtools関数内の残りは見る意味薄そうだし飛ばすか。
まぁ要は、実質strictmodeにする(ためにいろいろやる)関数って感じかな(知らん)

mountStateImplに戻る

ReactFiberHooks.js
function mountStateImpl<S>(initialState: (() => S) | S): Hook {
  const hook = mountWorkInProgressHook();
  if (typeof initialState === 'function') {
    const initialStateInitializer = initialState;
    // $FlowFixMe[incompatible-use]: Flow doesn't like mixed types
    initialState = initialStateInitializer();
    if (shouldDoubleInvokeUserFnsInHooksDEV) {
      setIsStrictModeForDevtools(true);
      try {
        // $FlowFixMe[incompatible-use]: Flow doesn't like mixed types
        initialStateInitializer();
      } finally {
        setIsStrictModeForDevtools(false);
      }
    }
  }
  hook.memoizedState = hook.baseState = initialState;
  const queue: UpdateQueue<S, BasicStateAction<S>> = {
    pending: null,
    lanes: NoLanes,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),
  };
  hook.queue = queue;
  return hook;
}
Yug (やぐ)Yug (やぐ)

mountWorkInProgressHookって何だったか忘れたので見直した

まぁ要はfiberが持ってるhookの連結リストを初期化する、もしくは追加していく、てことやね

とはいえそのhookは全プロパティがnullの初期化されたものなのは同じ。

んで最後にその初期状態のhookをreturnして終わり。

(何なら循環リストか?)
https://x.com/calloc134/status/1861254365543309792

Yug (やぐ)Yug (やぐ)

なのでその初期状態hookをhookとして取得。

次のスコープ

ReactFiberHooks.js
if (typeof initialState === 'function') {
  const initialStateInitializer = initialState;
  // $FlowFixMe[incompatible-use]: Flow doesn't like mixed types
  initialState = initialStateInitializer();
  if (shouldDoubleInvokeUserFnsInHooksDEV) {
    setIsStrictModeForDevtools(true);
    try {
      // $FlowFixMe[incompatible-use]: Flow doesn't like mixed types
      initialStateInitializer();
    } finally {
      setIsStrictModeForDevtools(false);
    }
  }
}
Yug (やぐ)Yug (やぐ)

更新用関数を実行するとこまではもうやったので割愛。

んでstrict modeのような2度実行が必要だったらstrict modeをtrueにする

んで2度目の更新用関数実行

最後にstrict mode(による2度実行の情報?)をオフ

Yug (やぐ)Yug (やぐ)

最後のスコープ

hook.memoizedState = hook.baseState = initialState;
const queue: UpdateQueue<S, BasicStateAction<S>> = {
  pending: null,
  lanes: NoLanes,
  dispatch: null,
  lastRenderedReducer: basicStateReducer,
  lastRenderedState: (initialState: any),
};
hook.queue = queue;
return hook;

更新用関数の実行結果としてのinitialStateもしくはstateそのものとしてのinitialStateをhook.baseStateとhook.memoizedStateに代入

Yug (やぐ)Yug (やぐ)

てかHook.baseStateってなんだ?

GPTに聞いたらなるほどぉになったのでメモ

まぁbaseStateがスナップショットつまり過去の値で、差分比較のために使われるbaseとしてのstateみたいな感じっぽいな

んでmemoizedStateは常に更新される最新のstate

Yug (やぐ)Yug (やぐ)

次の行、UpdateQueueってなんだ?

同ファイルに定義ある

ReactFiberHooks.js
export type UpdateQueue<S, A> = {
  pending: Update<S, A> | null,
  lanes: Lanes,
  dispatch: (A => mixed) | null,
  lastRenderedReducer: ((S, A) => S) | null,
  lastRenderedState: S | null,
};

Fiber.updateQueueと同じだと思うんだよなぁ、これはただのmixed型だったけど、どっかで繋がってるんじゃないかなぁという妄想
https://zenn.dev/link/comments/1ea92c3ab4da50

Yug (やぐ)Yug (やぐ)

とりあえずUpdateオブジェクトをpendingプロパティとして持っているな

Updateは同ファイルにある、おなじみのこれ

ReactFiberHooks.js
export type Update<S, A> = {
  lane: Lane,
  revertLane: Lane,
  action: A,
  hasEagerState: boolean,
  eagerState: S | null,
  next: Update<S, A>,
};

S, AはState, Actionだろうな

Yug (やぐ)Yug (やぐ)

まぁ要はupdateQueueはupdateオブジェクトやその他もろもろを格納してる

んでupdate自体は連結リストになってる

Yug (やぐ)Yug (やぐ)

復習。BasicStateActionは懐かしのこれ

ReactFiberHooks.js
type BasicStateAction<S> = (S => S) | S;
type Dispatch<A> = A => void;

つまりsetStateの引数

ReactFiberHooks.js
function mountState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {

setStateそのものがDispatch<BasicStateAction<S>>

Yug (やぐ)Yug (やぐ)

ということで戻ると、

ReactFiberHooks.js
hook.memoizedState = hook.baseState = initialState;
const queue: UpdateQueue<S, BasicStateAction<S>> = {
  pending: null,
  lanes: NoLanes,
  dispatch: null,
  lastRenderedReducer: basicStateReducer,
  lastRenderedState: (initialState: any),
};
hook.queue = queue;
return hook;

UpdateQueue型のqueueという変数を作ってる

だがpendingもdispatchもnullなので初期状態に近い?

とはいえlastRenderedState: (initialState: any)でinitialStateは入れてあげてるので完全初期状態ではないって感じ

Yug (やぐ)Yug (やぐ)

んで.memoizedState以外はnullのままの半分初期状態みたいな感じのhookのqueueプロパティにさっきのinitialStateを反映したqueueを代入してあげる

でそのhookをreturnして終了

Yug (やぐ)Yug (やぐ)

なのでmountStateImplをまとめると...

  • mountWorkInProgressHookにより、fiberが持ってるhookの連結リストを初期化/追加
  • 更新用関数だったら2度実行、その後strictmode(の情報?)をオフに
  • 最終的なinitialStateをhookに反映させる
  • 新しいinitialStateを反映したqueueもhook.queueに代入
  • 最後に結果としてできあがったhookをreturn

まぁ要はinitialStateをhookに反映させて、そのhookを返却するよ~ていう関数やね

Yug (やぐ)Yug (やぐ)

よっしゃ、mountStateImplが終わったのでやっとmountStateに戻れる、戻ろう

ReactFiberHooks.js
function mountState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  const hook = mountStateImpl(initialState);
  const queue = hook.queue;
  const dispatch: Dispatch<BasicStateAction<S>> = (dispatchSetState.bind(
    null,
    currentlyRenderingFiber,
    queue,
  ): any);
  queue.dispatch = dispatch;
  return [hook.memoizedState, dispatch];
}
Yug (やぐ)Yug (やぐ)

ふむふむ

  • const hook = mountStateImpl(initialState);でinitialStateを反映したhookを取得

  • そのhookの.queueをqueueという変数名で取得

  • dispatchという関数を作る

    • 型がDispatch<BasicStateAction<S>>であることからこのdispatchというのはsetState関数そのものであることがわかる
Yug (やぐ)Yug (やぐ)

だが代入の内容が複雑だな、言語化したい

const dispatch: Dispatch<BasicStateAction<S>> = (dispatchSetState.bind(
  null,
  currentlyRenderingFiber,
  queue,
): any);

あーbindか、前見たなぁ

ん、これか
https://zenn.dev/link/comments/3029716222f518

じゃあdispatchSetStateというのが何なのかさえわかれば良さそうだな

Yug (やぐ)Yug (やぐ)

同ファイルにあった。これ

ReactFiberHooks.js
function dispatchSetState<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A,
): void {
  if (__DEV__) {
    if (typeof arguments[3] === 'function') {
      console.error(
        "State updates from the useState() and useReducer() Hooks don't support the " +
          'second callback argument. To execute a side effect after ' +
          'rendering, declare it in the component body with useEffect().',
      );
    }
  }

  const lane = requestUpdateLane(fiber);
  const didScheduleUpdate = dispatchSetStateInternal(
    fiber,
    queue,
    action,
    lane,
  );
  if (didScheduleUpdate) {
    startUpdateTimerByLane(lane);
  }
  markUpdateInDevTools(fiber, lane, action);
}
Yug (やぐ)Yug (やぐ)

arguments使ってるな
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Functions/arguments

なるほど、bindしてるから呼び出されるときは引数省略されるとはいえ、dispatchSetStateとしてはすでに引数3つまで読んでることになってて、プラス1個つまりarguments[3]があった場合は、それはsetStateの引数に第二引数が呼ばれたということで、それはおかしいのでエラーを出すという感じだろうな

Yug (やぐ)Yug (やぐ)

次、const lane = requestUpdateLane(fiber);のrequestUpdateLaneとはなんだ

定義元これ

packages\react-reconciler\src\ReactFiberWorkLoop.js
export function requestUpdateLane(fiber: Fiber): Lane {
  // Special cases
  const mode = fiber.mode;
  if (!disableLegacyMode && (mode & ConcurrentMode) === NoMode) {
    return (SyncLane: Lane);
  } else if (
    (executionContext & RenderContext) !== NoContext &&
    workInProgressRootRenderLanes !== NoLanes
  ) {
    // This is a render phase update. These are not officially supported. The
    // old behavior is to give this the same "thread" (lanes) as
    // whatever is currently rendering. So if you call `setState` on a component
    // that happens later in the same render, it will flush. Ideally, we want to
    // remove the special case and treat them as if they came from an
    // interleaved event. Regardless, this pattern is not officially supported.
    // This behavior is only a fallback. The flag only exists until we can roll
    // out the setState warning, since existing code might accidentally rely on
    // the current behavior.
    return pickArbitraryLane(workInProgressRootRenderLanes);
  }

  const transition = requestCurrentTransition();
  if (transition !== null) {
    if (__DEV__) {
      if (!transition._updatedFibers) {
        transition._updatedFibers = new Set();
      }
      transition._updatedFibers.add(fiber);
    }

    const actionScopeLane = peekEntangledActionLane();
    return actionScopeLane !== NoLane
      ? // We're inside an async action scope. Reuse the same lane.
        actionScopeLane
      : // We may or may not be inside an async action scope. If we are, this
        // is the first update in that scope. Either way, we need to get a
        // fresh transition lane.
        requestTransitionLane(transition);
  }

  return eventPriorityToLane(resolveUpdatePriority());
}

fiberを受け取ってレーン(優先度)を返す

Yug (やぐ)Yug (やぐ)

じゃあもしかしてtransitionと遅延ローディング(lazy loading)ってやつ同じか?

Yug (やぐ)Yug (やぐ)

すごい長いコメントがある。直訳するとこうなる

これはレンダーフェーズのアップデートです。これらは公式にはサポートされていません。古い動作は、現在レンダリングしているものと同じ「スレッド」(レーン)を与えることです。そのため、同じレンダリングの後半で発生するコンポーネントで setState を呼び出すと、フラッシュされます。理想的には、特殊なケースを取り除き、インターリーブされたイベントから来たかのように扱いたい。いずれにせよ、このパターンは公式にはサポートされていません。この動作はフォールバックに過ぎない。既存のコードが誤って現在の動作に依存してしまう可能性があるため、setState警告をロールアウトできるようになるまで、このフラグは存在するだけです。

全然意味わからん。ちなみにインターリーブは「割り込む or 構成要素を入れ替える/交互に配置する」みたいな意味っぽい

https://ejje.weblio.jp/content/interleave#goog_rewarded

Yug (やぐ)Yug (やぐ)

うーん細かくは見ないことにするがとりあえず、引数で受け取ったfiberに適切なLaneを計算して返す、みたいな関数だろうな

前見たように、Fiberにも.lanesというプロパティがあるので

Yug (やぐ)Yug (やぐ)

dispatchSetStateに戻る

ReactFiberHooks.js
function dispatchSetState<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A,
): void {
  if (__DEV__) {
    if (typeof arguments[3] === 'function') {
      console.error(
        "State updates from the useState() and useReducer() Hooks don't support the " +
          'second callback argument. To execute a side effect after ' +
          'rendering, declare it in the component body with useEffect().',
      );
    }
  }

  const lane = requestUpdateLane(fiber);
  const didScheduleUpdate = dispatchSetStateInternal(
    fiber,
    queue,
    action,
    lane,
  );
  if (didScheduleUpdate) {
    startUpdateTimerByLane(lane);
  }
  markUpdateInDevTools(fiber, lane, action);
}
Yug (やぐ)Yug (やぐ)

次、dispatchSetStateInternalとは何だ?

すぐ下に定義があった

ReactFiberHooks.js
function dispatchSetStateInternal<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A,
  lane: Lane,
): boolean {
  const update: Update<S, A> = {
    lane,
    revertLane: NoLane,
    action,
    hasEagerState: false,
    eagerState: null,
    next: (null: any),
  };

  if (isRenderPhaseUpdate(fiber)) {
    enqueueRenderPhaseUpdate(queue, update);
  } else {
    const alternate = fiber.alternate;
    if (
      fiber.lanes === NoLanes &&
      (alternate === null || alternate.lanes === NoLanes)
    ) {
      // The queue is currently empty, which means we can eagerly compute the
      // next state before entering the render phase. If the new state is the
      // same as the current state, we may be able to bail out entirely.
      const lastRenderedReducer = queue.lastRenderedReducer;
      if (lastRenderedReducer !== null) {
        let prevDispatcher = null;
        if (__DEV__) {
          prevDispatcher = ReactSharedInternals.H;
          ReactSharedInternals.H = InvalidNestedHooksDispatcherOnUpdateInDEV;
        }
        try {
          const currentState: S = (queue.lastRenderedState: any);
          const eagerState = lastRenderedReducer(currentState, action);
          // Stash the eagerly computed state, and the reducer used to compute
          // it, on the update object. If the reducer hasn't changed by the
          // time we enter the render phase, then the eager state can be used
          // without calling the reducer again.
          update.hasEagerState = true;
          update.eagerState = eagerState;
          if (is(eagerState, currentState)) {
            // Fast path. We can bail out without scheduling React to re-render.
            // It's still possible that we'll need to rebase this update later,
            // if the component re-renders for a different reason and by that
            // time the reducer has changed.
            // TODO: Do we still need to entangle transitions in this case?
            enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
            return false;
          }
        } catch (error) {
          // Suppress the error. It will throw again in the render phase.
        } finally {
          if (__DEV__) {
            ReactSharedInternals.H = prevDispatcher;
          }
        }
      }
    }

    const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
    if (root !== null) {
      scheduleUpdateOnFiber(root, fiber, lane);
      entangleTransitionUpdate(root, queue, lane);
      return true;
    }
  }
  return false;
}
Yug (やぐ)Yug (やぐ)

fiber, queue, action. laneを受け取ってbooleanだけを返すのか

とりあえず最初のisRenderPhaseUpdateとenqueueRenderPhaseUpdateを見てみる

同ファイルに定義がある

ReactFiberHooks.js
function isRenderPhaseUpdate(fiber: Fiber): boolean {
  const alternate = fiber.alternate;
  return (
    fiber === currentlyRenderingFiber ||
    (alternate !== null && alternate === currentlyRenderingFiber)
  );
}

function enqueueRenderPhaseUpdate<S, A>(
  queue: UpdateQueue<S, A>,
  update: Update<S, A>,
): void {
  // This is a render phase update. Stash it in a lazily-created map of
  // queue -> linked list of updates. After this render pass, we'll restart
  // and apply the stashed updates on top of the work-in-progress hook.
  didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate =
    true;
  const pending = queue.pending;
  if (pending === null) {
    // This is the first update. Create a circular list.
    update.next = update;
  } else {
    update.next = pending.next;
    pending.next = update;
  }
  queue.pending = update;
}
Yug (やぐ)Yug (やぐ)

isRenderPhaseUpdateはfiberを受け取って、そのfiberがrender中のものかどうかをbooleanで返す

enqueueRenderPhaseUpdateはqueue, updateを受け取って、updateオブジェクトの循環リストを作っていく感じ

Yug (やぐ)Yug (やぐ)

GPT

たしかにrender中にsetStateが呼ばれたみたいなときにここを通過するのかも。
だから特別な処理が行われるのも納得できる

Yug (やぐ)Yug (やぐ)

つまりこの部分は、「render中だったらそのupdateを循環リストに突っ込んどく」みたいなことをしてる感じか

if (isRenderPhaseUpdate(fiber)) {
  enqueueRenderPhaseUpdate(queue, update);
}

じゃあレンダー中じゃなかったらどうするの?すぐ更新を反映するみたいなこと?

ということでelse下を見ていく

ReactFiberHooks.js
function dispatchSetStateInternal<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A,
  lane: Lane,
): boolean {
  const update: Update<S, A> = {
    lane,
    revertLane: NoLane,
    action,
    hasEagerState: false,
    eagerState: null,
    next: (null: any),
  };

  if (isRenderPhaseUpdate(fiber)) {
    enqueueRenderPhaseUpdate(queue, update);
  } else {
    const alternate = fiber.alternate;
    if (
      fiber.lanes === NoLanes &&
      (alternate === null || alternate.lanes === NoLanes)
    ) {
      // The queue is currently empty, which means we can eagerly compute the
      // next state before entering the render phase. If the new state is the
      // same as the current state, we may be able to bail out entirely.
      const lastRenderedReducer = queue.lastRenderedReducer;
      if (lastRenderedReducer !== null) {
        let prevDispatcher = null;
        if (__DEV__) {
          prevDispatcher = ReactSharedInternals.H;
          ReactSharedInternals.H = InvalidNestedHooksDispatcherOnUpdateInDEV;
        }
        try {
          const currentState: S = (queue.lastRenderedState: any);
          const eagerState = lastRenderedReducer(currentState, action);
          // Stash the eagerly computed state, and the reducer used to compute
          // it, on the update object. If the reducer hasn't changed by the
          // time we enter the render phase, then the eager state can be used
          // without calling the reducer again.
          update.hasEagerState = true;
          update.eagerState = eagerState;
          if (is(eagerState, currentState)) {
            // Fast path. We can bail out without scheduling React to re-render.
            // It's still possible that we'll need to rebase this update later,
            // if the component re-renders for a different reason and by that
            // time the reducer has changed.
            // TODO: Do we still need to entangle transitions in this case?
            enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
            return false;
          }
        } catch (error) {
          // Suppress the error. It will throw again in the render phase.
        } finally {
          if (__DEV__) {
            ReactSharedInternals.H = prevDispatcher;
          }
        }
      }
    }

    const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
    if (root !== null) {
      scheduleUpdateOnFiber(root, fiber, lane);
      entangleTransitionUpdate(root, queue, lane);
      return true;
    }
  }
  return false;
}
Yug (やぐ)Yug (やぐ)

というか更新はレンダー中ではないのが普通よな。
ボタンを連続でクリックしまくるみたいな時にのみrender中になってしまう可能性がある程度だと思うので。そんなの特殊ケースな気がする

Yug (やぐ)Yug (やぐ)

fiberもしくはalternateのlanesがNoLanesだった時のみ通過する ←??

if (
  fiber.lanes === NoLanes &&
  (alternate === null || alternate.lanes === NoLanes)
)

ちなみにNoLaneはレーンの中で一番優先度が高いデフォルト値

特殊なレーンとして「NoLane」という全てのビットが0がレーンがあります。
これはJavaScriptでいう所のundefinedのような、レーンを何も設定していない際のデフォルト値であり、最も高い優先度です。(もっとも、「NoLane」を優先度として使うことは稀です)

https://zenn.dev/ktmouk/articles/68fefedb5fcbdc#レーンは優先度を表す

んー、てことはfiberができたてホヤホヤだからレーンもデフォルト値のままで、その場合にのみ処理をしていきたい、みたいなことなのだろうか

Yug (やぐ)Yug (やぐ)
// The queue is currently empty, which means we can eagerly compute the
// next state before entering the render phase. If the new state is the
// same as the current state, we may be able to bail out entirely.
const lastRenderedReducer = queue.lastRenderedReducer;

コメントを直訳するとこれ

キューは現在空なので、レンダーフェーズに入る前に次の状態を熱心に計算することができる。新しい状態が現在の状態と同じなら、完全にドタキャンできるかもしれない。

bail outは救済という意味だが、どうやらドタキャンという意味もあるらしく、それはつまりrenderをスキップするという意味のドタキャンでは無いだろうか。「新しい状態が現在の状態と同じなら」って言ってるし
https://eigolab.net/212

それを頭の隅に入れた状態で読み進めていく

Yug (やぐ)Yug (やぐ)
const lastRenderedReducer = queue.lastRenderedReducer;

queueはUpdateQueue型なので復習すると、こんな型

export type UpdateQueue<S, A> = {
  pending: Update<S, A> | null,
  lanes: Lanes,
  dispatch: (A => mixed) | null,
  lastRenderedReducer: ((S, A) => S) | null,
  lastRenderedState: S | null,
};

なのでlastRenderedReducerはstateとactionを受け取ってstateを返す(もしくはnull)ことがわかる

あーこれはつまりuseReducerのreducerそのものだな。useStateは関係無さそうだし飛ばすか

function reducer(state, action) {
  // ...
}

function MyComponent() {
  const [state, dispatch] = useReducer(reducer, { age: 42 });
  // ...

https://ja.react.dev/reference/react/useReducer#usereducer

おそらく、スナップショットとしてのreducer、つまり直近で使われてた古いreducer的な意味合いか?

Yug (やぐ)Yug (やぐ)
if (lastRenderedReducer !== null) {
  let prevDispatcher = null;
  if (__DEV__) {
    prevDispatcher = ReactSharedInternals.H;
    ReactSharedInternals.H = InvalidNestedHooksDispatcherOnUpdateInDEV;
  }
  • lastRenderedReducerがnullじゃなかったらif文通過。

  • prevDispatcherをnullで初期化。

  • __DEV__つまり開発環境だったら更にif文通過。

  • prevDispatcherに懐かしのReactSharedInternals.Hを代入。ReactSharedInternals.Hはフックがたくさん入ってるDispatcher型のやつ↓

packages\react\src\ReactSharedInternalsClient.js
export type SharedStateClient = {
  H: null | Dispatcher, // ReactCurrentDispatcher for Hooks
  ...

const ReactSharedInternals: SharedStateClient = ({
  H: null,
  ...

export default ReactSharedInternals;
  • ReactSharedInternals.HにInvalidNestedHooksDispatcherOnUpdateInDEVを代入。
    InvalidNestedHooksDispatcherOnUpdateInDEVってやつもDispatcherか。
ReactFiberHooks.js
let InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher | null = null;
Yug (やぐ)Yug (やぐ)

InvalidNestedHooksDispatcherOnUpdateInDEVの代入を見てみたい

これ

InvalidNestedHooksDispatcherOnUpdateInDEV = {
  ...
  useState<S>(
    initialState: (() => S) | S,
  ): [S, Dispatch<BasicStateAction<S>>] {
    currentHookNameInDev = 'useState';
    warnInvalidHookAccess();
    updateHookTypesDev();
    const prevDispatcher = ReactSharedInternals.H;
    ReactSharedInternals.H = InvalidNestedHooksDispatcherOnUpdateInDEV;
    try {
      return updateState(initialState);
    } finally {
      ReactSharedInternals.H = prevDispatcher;
    }
  },
  ...

warnInvalidHookAccess()はこれ

ReactFiberHooks.js
const warnInvalidHookAccess = () => {
  console.error(
    'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks. ' +
      'You can only call Hooks at the top level of your React function. ' +
      'For more information, see ' +
      'https://react.dev/link/rules-of-hooks',
  );
};

なのでフックが無効な使い方されたときの警告をだすためだけのフックだな

たしかにInvalidNestedHooksDispatcherOnUpdateInDEVという名前の通り。

Yug (やぐ)Yug (やぐ)

うーんなんでlastRenderedReducerが存在するかつ__DEV__だったら無効なフックになるのかわからんが、次にいくか

Yug (やぐ)Yug (やぐ)
ReactFiberHooks.js
try {
  const currentState: S = (queue.lastRenderedState: any);
  const eagerState = lastRenderedReducer(currentState, action);

queue.lastRenderedStateというのはさっきの「直近のreducer」のstate版かな
つまり「直近のstate」。差分で使う感じやろな

export type UpdateQueue<S, A> = {
  pending: Update<S, A> | null,
  lanes: Lanes,
  dispatch: (A => mixed) | null,
  lastRenderedReducer: ((S, A) => S) | null,
  lastRenderedState: S | null,
};

ん、でもそれってHook.baseStateと同じじゃね...?使い分けが気になるが一旦無視
https://zenn.dev/link/comments/9ecea1d72a042e

Yug (やぐ)Yug (やぐ)

とりあえずcurrentStateには直近の古いstateを代入してるということが言える

const currentState: S = (queue.lastRenderedState: any);

次の行

const eagerState = lastRenderedReducer(currentState, action);

このlastRenderedReducerという関数はさっき代入したやつ

const lastRenderedReducer = queue.lastRenderedReducer;

つまり直近の古いreducer関数へのリンク

export type UpdateQueue<S, A> = {
  pending: Update<S, A> | null,
  lanes: Lanes,
  dispatch: (A => mixed) | null,
  lastRenderedReducer: ((S, A) => S) | null,
  lastRenderedState: S | null,
};
Yug (やぐ)Yug (やぐ)

なのでその古いreducer関数を実行してることになる

const eagerState = lastRenderedReducer(currentState, action);

引数はcurrentState(古い直近のstate)とaction。その返り値がeagerStateとして代入される。

なるほど、たしかにreducerは更新処理を実行し、結果としてのstateを返すのでそれをeagerStateとして取得する訳か

Yug (やぐ)Yug (やぐ)

// Stash the eagerly computed state, and the reducer used to compute
// it, on the update object. If the reducer hasn't changed by the
// time we enter the render phase, then the eager state can be used
// without calling the reducer again.
update.hasEagerState = true;
update.eagerState = eagerState;

コメントを直訳

熱心に計算されたstateと、それを計算するために使用されたreducerをupdateオブジェクトに格納します。レンダリングフェーズに入るまでにリデューサーが変更されていなければ、リデューサーを再度呼び出すことなく、eagerされたstateを使用することができます。

あーstashって「へそくり」とか出てくるから意味をよくわかっていなかったんだけど、「格納」って意味なんだ

Yug (やぐ)Yug (やぐ)

そんなことしてなくない?stateを入れてるだけでreducerは入れてないように見えるが...一旦飛ばす

熱心に計算されたstateと、それを計算するために使用されたreducerをupdateオブジェクトに格納します

Yug (やぐ)Yug (やぐ)

ReactFiberHooks.js
if (is(eagerState, currentState)) {
  // Fast path. We can bail out without scheduling React to re-render.
  // It's still possible that we'll need to rebase this update later,
  // if the component re-renders for a different reason and by that
  // time the reducer has changed.
  // TODO: Do we still need to entangle transitions in this case?
  enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
  return false;
}

is()とは?Object.isじゃないのか?

Yug (やぐ)Yug (やぐ)

定義元ファイル、これ

packages\shared\objectIs.js
/**
 * inlined Object.is polyfill to avoid requiring consumers ship their own
 * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is
 */
function is(x: any, y: any) {
  return (
    (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y) // eslint-disable-line no-self-compare
  );
}

const objectIs: (x: any, y: any) => boolean =
  // $FlowFixMe[method-unbinding]
  typeof Object.is === 'function' ? Object.is : is;

export default objectIs;

お、激アツだ!「Object.isって差分検出で実際に使われてんのか?」てずっと気になってたので見れて良かった

state の同一性の比較は、Object.is によって行われます

https://ja.react.dev/reference/react/useState#setstate-caveats

Object.isだけじゃなくて、うまく拡張してる感じか

Yug (やぐ)Yug (やぐ)

今回、isという名前でimportしてるが、

ReactFiberHooks.js
import is from 'shared/objectIs';

実態としてはobjectIs関数なので、「isという名前でObject.isを使う慣習がある」と把握しておいても良さそうやな

objectIs.js
export default objectIs;
Yug (やぐ)Yug (やぐ)

コメント直訳

Object.isポリフィルをインライン化し、コンシューマが独自に出荷する必要がないようにした。

ポリフィルとは互換性保つためのコード的な感じか
https://developer.mozilla.org/ja/docs/Glossary/Polyfill

Object.isの型が関数だったら普通にObject.is使うけど、もし関数じゃなかったらisという独自関数を使ってObject.isの代わりにするみたいな感じなのかな。

関数じゃない場合と言うのは、そうなってしまうReactユーザの環境?が多分あるんだろうな。

とりあえず、is関数というポリフィル(互換関数)も用意してあるよ~という感じ。

GPT
なるほど、Object.isはES6からなのか

Object.is は ES6(ECMAScript 2015)で導入されたため、古いブラウザや環境では利用できない場合があります。

  • 環境が Object.is をサポートしている場合にのみ、そのまま利用します。
  • もしサポートされていない場合は、is 関数というポリフィル(互換関数)を代わりに使用します。

なるほど

例えば、ES5以前の環境では Object.is が存在しません。

Yug (やぐ)Yug (やぐ)

ということで戻る

ReactFiberHooks.js
if (is(eagerState, currentState)) {
  // Fast path. We can bail out without scheduling React to re-render.
  // It's still possible that we'll need to rebase this update later,
  // if the component re-renders for a different reason and by that
  // time the reducer has changed.
  // TODO: Do we still need to entangle transitions in this case?
  enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
  return false;
}

is()の引数の2つはこんな感じなので、

const currentState: S = (queue.lastRenderedState: any);
const eagerState = lastRenderedReducer(currentState, action);

以下2つを比較すると言える。

  1. currentState = 直近の古いstate
  2. eagerState = 直近の古いreducerを使っているがその引数のactionは新しいやつで、その実行結果としてのstate
    • actionはbindされた結果としてのdispatch関数(実質setState関数)の第一引数に渡されるものなので、新しく渡されたものと考えて良いはず(mountState関数から辿ればそう判断できる)
Yug (やぐ)Yug (やぐ)
if (is(eagerState, currentState)) {
  // Fast path. We can bail out without scheduling React to re-render.
  // It's still possible that we'll need to rebase this update later,
  // if the component re-renders for a different reason and by that
  // time the reducer has changed.
  // TODO: Do we still need to entangle transitions in this case?
  enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
  return false;
}

コメントを直訳

高速パス。Reactの再レンダリングをスケジューリングすることなくベイルアウト(renderをドタキャン)できる。
コンポーネントが別の理由で再レンダリングし、その時までにreducerが変更されていた場合、この更新を後でリベースする必要がある可能性はまだある。
TODO: この場合でもまだtransitionsを絡める必要があるか?

  • renderをドタキャンできるケースのことをFast path/高速パスと呼ぶっぽい
  • 「でもまだリベースすべきかもしれんから油断すんな」 ← ???
  • TODOで問いかけてんのは何?誰に対しての問いかけ?そしてどういう意味?
Yug (やぐ)Yug (やぐ)

ていうかふと思ったことメモ:

直近のreducerがあった場合のみif通過してきてるけど、useReducerはまったく使わずにただuseStateだけ使ってた場合はどうなるんだ?そうなるとreducerなんて存在しないはずだと思うんだが。

reducerに比重を置きまくってるのが違和感。確かにuseReducerもstate更新に関わるとはいえ。

Yug (やぐ)Yug (やぐ)

あとdispatchSetStateInternalの返り値であるtrue/falseは何の真偽値なのかも確認したい

一応GPTメモ

Yug (やぐ)Yug (やぐ)

GPT

そりゃそうじゃね?としか思わないというか、よくわからない...

Yug (やぐ)Yug (やぐ)

なるほど、確かにソースコードのコメントなんだからReact開発者へのメッセージか。

このスキップする処理がtransitionに関係するなら、考えないといけないことがあるかもよ、みたいなことか。transition自体よくわからんのでイメージできないが、とりあえずok

Yug (やぐ)Yug (やぐ)

てことはこのenqueueConcurrentHookUpdateAndEagerlyBailout関数はrenderをスキップする処理なんじゃなかろうか。見てみる

ReactFiberHooks.js
if (is(eagerState, currentState)) {
  // Fast path. We can bail out without scheduling React to re-render.
  // It's still possible that we'll need to rebase this update later,
  // if the component re-renders for a different reason and by that
  // time the reducer has changed.
  // TODO: Do we still need to entangle transitions in this case?
  enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
  return false;
}

これ

packages\react-reconciler\src\ReactFiberConcurrentUpdates.js
export function enqueueConcurrentHookUpdateAndEagerlyBailout<S, A>(
  fiber: Fiber,
  queue: HookQueue<S, A>,
  update: HookUpdate<S, A>,
): void {
  // This function is used to queue an update that doesn't need a rerender. The
  // only reason we queue it is in case there's a subsequent higher priority
  // update that causes it to be rebased.
  const lane = NoLane;
  const concurrentQueue: ConcurrentQueue = (queue: any);
  const concurrentUpdate: ConcurrentUpdate = (update: any);
  enqueueUpdate(fiber, concurrentQueue, concurrentUpdate, lane);

  // Usually we can rely on the upcoming render phase to process the concurrent
  // queue. However, since this is a bail out, we're not scheduling any work
  // here. So the update we just queued will leak until something else happens
  // to schedule work (if ever).
  //
  // Check if we're currently in the middle of rendering a tree, and if not,
  // process the queue immediately to prevent a leak.
  const isConcurrentlyRendering = getWorkInProgressRoot() !== null;
  if (!isConcurrentlyRendering) {
    finishQueueingConcurrentUpdates();
  }
}
Yug (やぐ)Yug (やぐ)

引数の型で使われてるHookQueueとHookUpdateってなんや?て思ったけどなぜかasで名前変えてimportしてるだけで、実態はおなじみUpdateQueueとUpdateのこと

ReactFiberConcurrentUpdates.js
import type {
  UpdateQueue as HookQueue,
  Update as HookUpdate,
} from './ReactFiberHooks';
Yug (やぐ)Yug (やぐ)
// This function is used to queue an update that doesn't need a rerender. The
// only reason we queue it is in case there's a subsequent higher priority
// update that causes it to be rebased.

この関数は、再レンダーを必要としない更新をキューに入れるために使用される。

へぇ、再レンダー不要のupdateもキューに突っ込むんだ。
...なんで?保持しておく理由なんかあるのか?変更が起こらないupdateなんてもはやupdate(更新)ではないから捨てちゃえば良いのに。使うのか?

キューに入れる唯一の理由は、その後に優先順位の高い更新があり、その更新がリベースされる場合です。

うーん、よく意味がわからない。rebase(状態の再適用)することになったらそのupdateを元にrebaseしたいからupdateは必要だ、みたいな感じ?わからん。飛ばす

Yug (やぐ)Yug (やぐ)
ReactFiberConcurrentUpdates.js
const lane = NoLane;
const concurrentQueue: ConcurrentQueue = (queue: any);
const concurrentUpdate: ConcurrentUpdate = (update: any);

laneをNoLaneで初期化。

ConcurrentQueueとConcurrentUpdateとは?さっきと同じでUpdateQueueとUpdateのことか?

→違った。独自に作られたやつか。UpdateQueueとUpdateを簡易化したというか、不要なプロパティをなくした版みたいな感じか

ReactFiberConcurrentUpdates.js
export type ConcurrentUpdate = {
  next: ConcurrentUpdate,
  lane: Lane,
};

type ConcurrentQueue = {
  pending: ConcurrentUpdate | null,
};

UpdateQueueとUpdateはこれ。再掲

ReactFiberHooks.js
export type Update<S, A> = {
  lane: Lane,
  revertLane: Lane,
  action: A,
  hasEagerState: boolean,
  eagerState: S | null,
  next: Update<S, A>,
};

export type UpdateQueue<S, A> = {
  pending: Update<S, A> | null,
  lanes: Lanes,
  dispatch: (A => mixed) | null,
  lastRenderedReducer: ((S, A) => S) | null,
  lastRenderedState: S | null,
};
Yug (やぐ)Yug (やぐ)

でもそれだと代入できないはずでは?

ReactFiberConcurrentUpdates.js
const lane = NoLane;
const concurrentQueue: ConcurrentQueue = (queue: any);
const concurrentUpdate: ConcurrentUpdate = (update: any);

代入されてるqueueとupdateは引数で渡ってきたものであって、それは実質UpdateQueueとUpdateだからなぁ。ConcurrentQueueとConcurrentUpdateには適合できないはず

ReactFiberConcurrentUpdates.js
export function enqueueConcurrentHookUpdateAndEagerlyBailout<S, A>(
  fiber: Fiber,
  queue: HookQueue<S, A>,
  update: HookUpdate<S, A>,
): void {

うーん、flowでは一致したプロパティがあるなら強制的に型変換することが可能なのか?

そう理解するしかないな

Yug (やぐ)Yug (やぐ)

ということで次

ReactFiberConcurrentUpdates.js
enqueueUpdate(fiber, concurrentQueue, concurrentUpdate, lane);

enqueueUpdateはこれ

ReactFiberConcurrentUpdates.js
function enqueueUpdate(
  fiber: Fiber,
  queue: ConcurrentQueue | null,
  update: ConcurrentUpdate | null,
  lane: Lane,
) {
  // Don't update the `childLanes` on the return path yet. If we already in
  // the middle of rendering, wait until after it has completed.
  concurrentQueues[concurrentQueuesIndex++] = fiber;
  concurrentQueues[concurrentQueuesIndex++] = queue;
  concurrentQueues[concurrentQueuesIndex++] = update;
  concurrentQueues[concurrentQueuesIndex++] = lane;

  concurrentlyUpdatedLanes = mergeLanes(concurrentlyUpdatedLanes, lane);

  // The fiber's `lane` field is used in some places to check if any work is
  // scheduled, to perform an eager bailout, so we need to update it immediately.
  // TODO: We should probably move this to the "shared" queue instead.
  fiber.lanes = mergeLanes(fiber.lanes, lane);
  const alternate = fiber.alternate;
  if (alternate !== null) {
    alternate.lanes = mergeLanes(alternate.lanes, lane);
  }
}

concurrentQueuesとconcurrentQueuesIndexはこれ

ReactFiberConcurrentUpdates.js
// If a render is in progress, and we receive an update from a concurrent event,
// we wait until the current render is over (either finished or interrupted)
// before adding it to the fiber/hook queue. Push to this array so we can
// access the queue, fiber, update, et al later.
const concurrentQueues: Array<any> = [];
let concurrentQueuesIndex = 0;
Yug (やぐ)Yug (やぐ)

レンダーが進行中であり、並行イベントからupdateを受け取った場合、現在のレンダリングが完了するか中断されるまで、updateをfiber/hookキューに追加するのを待ちます。
この配列に追加しておき、後でキューや fiber、updateなどにアクセスできるようにします。

ふむふむ、updateを受け取ったとしてもそれがrender中だったら、そのupdateをfiber/hookのqueueに入れたくないので、このconcurrentQueuesに入れておくよ、みたいな感じか

確かに前見た通りFiber.updateQueueHook.baseQueueもupdateの循環リストなので、そこに直接入れないよ、てことね

https://zenn.dev/link/comments/6cdf49fc10ccaf
https://zenn.dev/link/comments/a33a17eca75668

へぇ、てことは直接fiber/hookのキューに入れるupdateはrender中では無いやつのみなんだ。

とはいえrenderが終わったらrender中のupdate関連であるfiber, update, キューとかにアクセスできるようにするために、concurrentQueuesにそれらの情報をpushしておくのね。

へぇぇ、なんかいろいろ考えられててすごいなぁ

Yug (やぐ)Yug (やぐ)

...ただ、そうなると矛盾が生じないか?

今見てるコードの大元はこのdispatchSetStateInternalな訳だが、

ReactFiberHooks.js
function dispatchSetStateInternal<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A,
  lane: Lane,
): boolean {
  const update: Update<S, A> = {
    lane,
    revertLane: NoLane,
    action,
    hasEagerState: false,
    eagerState: null,
    next: (null: any),
  };

  if (isRenderPhaseUpdate(fiber)) {
    enqueueRenderPhaseUpdate(queue, update);
  } else {
    const alternate = fiber.alternate;
    if (
      fiber.lanes === NoLanes &&
      (alternate === null || alternate.lanes === NoLanes)
    ) {
      // The queue is currently empty, which means we can eagerly compute the
      // next state before entering the render phase. If the new state is the
      // same as the current state, we may be able to bail out entirely.
      const lastRenderedReducer = queue.lastRenderedReducer;
      if (lastRenderedReducer !== null) {
        let prevDispatcher = null;
        if (__DEV__) {
          prevDispatcher = ReactSharedInternals.H;
          ReactSharedInternals.H = InvalidNestedHooksDispatcherOnUpdateInDEV;
        }
        try {
          const currentState: S = (queue.lastRenderedState: any);
          const eagerState = lastRenderedReducer(currentState, action);
          // Stash the eagerly computed state, and the reducer used to compute
          // it, on the update object. If the reducer hasn't changed by the
          // time we enter the render phase, then the eager state can be used
          // without calling the reducer again.
          update.hasEagerState = true;
          update.eagerState = eagerState;
          if (is(eagerState, currentState)) {
            // Fast path. We can bail out without scheduling React to re-render.
            // It's still possible that we'll need to rebase this update later,
            // if the component re-renders for a different reason and by that
            // time the reducer has changed.
            // TODO: Do we still need to entangle transitions in this case?
            enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
            return false;
          }
        } catch (error) {
          // Suppress the error. It will throw again in the render phase.
        } finally {
          if (__DEV__) {
            ReactSharedInternals.H = prevDispatcher;
          }
        }
      }
    }

    const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
    if (root !== null) {
      scheduleUpdateOnFiber(root, fiber, lane);
      entangleTransitionUpdate(root, queue, lane);
      return true;
    }
  }
  return false;
}

今はこのisRenderPhaseUpdate(fiber)の結果がfalseとしてelse内に入っているので、render中の更新ではないことが確定しているはず。

ReactFiberHooks.js
if (isRenderPhaseUpdate(fiber)) {
  enqueueRenderPhaseUpdate(queue, update);
} else {

render中ではないスコープなのに、そこでrender中のupdateのためのenqueue処理をしているということよね。これっておかしくないか???

Yug (やぐ)Yug (やぐ)

メモ:render中に更新するというのはコンポーネントのトップレベルでsetState実行するのと同じはずなので、それによる無限ループというあの謎を解明することに繋がるかもしれない...
(あれ、0を0に変更するのでも無限ループするのが謎なんだよな...)

function MyComponent() {
  const [count, setCount] = useState(0);

  // render中に状態更新を行う
  setCount(count + 1); // NG: render中に状態更新を発生させている
  return <div>{count}</div>;
}
Yug (やぐ)Yug (やぐ)

ふーむ、GPTに聞いたらこんな感じだった

  1. isRenderPhaseUpdate(fiber)がfalseであることは、確かに「現在のfiberがrenderフェーズの更新対象でない」ことを意味する
  2. だが、Reactアプリ全体では「並行的なレンダリング」が進行している可能性があり、その場合レンダリング中のfiber以外から並行して更新が発生し得る
  3. concurrentQueuesはその並行的に発生した更新を一時的に溜めておく役割(後続で安全に処理する)
  4. つまりrender中でなくても並行して別のレンダリングやイベント処理が発生する可能性があり、そういったものをカバーするのがconcurrentQueuesである

疑問

  1. 並行レンダリングとは?まさかレンダーって並行処理されてんの?
    • レンダーなのかレンダリングなのか
  2. concurrentQueueとconcurrentQueuesの違いは?
  3. render中でなくても並行レンダーによるupdateをconcurrentQueuesが受け取るのはわかったが、render中のupdateも受け取るという認識で良い?コードのコメントを見る限りそうだと思うんだが