Reactの内部実装を見ていくよ(useStateフック&ステート更新からのトリガーフェーズ)
やっていき
まずここを参考に
useState本体
export function useState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
resolveDispatcher.js
function resolveDispatcher() {
const dispatcher = ReactSharedInternals.H;
if (__DEV__) {
if (dispatcher === null) {
console.error(
'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
' one of the following reasons:\n' +
'1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
'2. You might be breaking the Rules of Hooks\n' +
'3. You might have more than one copy of React in the same app\n' +
'See https://react.dev/link/invalid-hook-call for tips about how to debug and fix this problem.',
);
}
}
// Will result in a null access error if accessed outside render phase. We
// intentionally don't throw our own error because this is in a hot path.
// Also helps ensure this is inlined.
return ((dispatcher: any): Dispatcher);
}
ReactSharedInternals.H;
はどこからやってくるのじゃ〜〜〜〜、となる
なんとその答えは別パッケージにある
react-reconcilerを参照
renderWithHooks関数内
https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactFiberHooks.js
export function renderWithHooks<Props, SecondArg>(
current: Fiber | null,
workInProgress: Fiber,
Component: (p: Props, arg: SecondArg) => any,
props: Props,
secondArg: SecondArg,
nextRenderLanes: Lanes,
): any {
renderLanes = nextRenderLanes;
currentlyRenderingFiber = workInProgress;
(省略)
if (__DEV__) {
if (current !== null && current.memoizedState !== null) {
ReactSharedInternals.H = HooksDispatcherOnUpdateInDEV;
} else if (hookTypesDev !== null) {
// This dispatcher handles an edge case where a component is updating,
// but no stateful hooks have been used.
// We want to match the production code behavior (which will use HooksDispatcherOnMount),
// but with the extra DEV validation to ensure hooks ordering hasn't changed.
// This dispatcher does that.
ReactSharedInternals.H = HooksDispatcherOnMountWithHookTypesInDEV;
} else {
ReactSharedInternals.H = HooksDispatcherOnMountInDEV;
}
} else {
ReactSharedInternals.H =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
}
...
ざっくり
currentがnull もしくはcurrentがあってもmemorizedStateがnullなら
初回のマウント時だと判断してHooksDispatcherOnMountをいれる
そうじゃないなら二回目以降の更新時だと判断してHooksDispatcherOnUpdateをいれる
という感じ?
currentというのはFiberツリーのcurrentのことだろうか?違うかも
HooksDispatcherOnMountとは
const HooksDispatcherOnMount: Dispatcher = {
readContext,
use,
useCallback: mountCallback,
useContext: readContext,
useEffect: mountEffect,
useImperativeHandle: mountImperativeHandle,
useLayoutEffect: mountLayoutEffect,
useInsertionEffect: mountInsertionEffect,
useMemo: mountMemo,
useReducer: mountReducer,
useRef: mountRef,
useState: mountState,
useDebugValue: mountDebugValue,
useDeferredValue: mountDeferredValue,
useTransition: mountTransition,
useSyncExternalStore: mountSyncExternalStore,
useId: mountId,
};
const HooksDispatcherOnUpdate: Dispatcher = {
readContext,
use,
useCallback: updateCallback,
useContext: readContext,
useEffect: updateEffect,
useImperativeHandle: updateImperativeHandle,
useInsertionEffect: updateInsertionEffect,
useLayoutEffect: updateLayoutEffect,
useMemo: updateMemo,
useReducer: updateReducer,
useRef: updateRef,
useState: updateState,
useDebugValue: updateDebugValue,
useDeferredValue: updateDeferredValue,
useTransition: updateTransition,
useSyncExternalStore: updateSyncExternalStore,
useId: updateId,
};
え、なにこれは
とりあえず、useStateの処理を追いたい場合にはmountState関数かupdateState関数を追跡すればいいらしい
- 初回のマウント時の場合
HooksDispatcherOnMountはmountState関数 - 二回目の更新時の場合
HooksDispatcherOnUpdateはupdateState関数
mountState/updateStateはどこから?
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];
}
function updateState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
return updateReducer(basicStateReducer, initialState);
}
いよいよここにたどり着いた
まずmountStateの感想
- mountStateImplでフックのオブジェクトを取得している?
- dispatchSetStateで現在レンダリング中のFiberオブジェクトとフックの持つキューをバインドして、更新用関数を作成している?
- ステートの閲覧する方はフック内部のmemorizedStateというプロパティを参照している
- 配列に入れて返却!いつもよく見るステートの戻り値や!
mountStateImplとは何をしているのか?
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;
}
とりあえずまず呼び出されているmountWorkInProgressHookとは?
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とcurrentlyRenderingFiber.memoizedStateに現在のフックを代入
現在のフックが二個目以降のフックなら
workInProgressHookとworkInProgressHook.nextに現在のフックを代入←???
workInProgressHookを返却
ここでヒント!
export type Hook = {
memoizedState: any,
baseState: any,
baseQueue: Update<any, any> | null,
queue: any,
next: Hook | null,
};
フックの型はこうなっているっぽい
hook.memoizedState: メモリに保持されているローカルな状態。
hook.baseState: hook.baseQueue内のすべてのアップデートオブジェクトがマージされた後の状態。
hook.baseQueue: 現在のレンダリング優先度よりも高いものだけを含む、アップデートオブジェクトの循環的なチェーン。
hook.queue: 優先度の高いすべてのアップデートオブジェクトを含む、アップデートオブジェクトの循環的なチェーン。
hook.next: 次のポインタ、チェーンの次のフックを指します。
つまり、フックオブジェクトは連結リストになっているっぽい
なお、nextはnull許容なので、次のフックオブジェクトがない場合も当然ある
なんとなく、フックの呼び出しが連結リストになっていることは把握したかも?
では、mountWorkInProgressHookの調査を終わり、mountStateImplに戻る
コード再掲
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;
}
感想
- initialStateはuseStateで最初に渡す値な気がする
- コールバック関数が渡されてたらそれを実行する?
useState(() => うんちゃら) //みたいな?
- initialStateを、memorizedStateとmemorizedStateに代入
- hook.queueに空っぽのキューを代入して初期化
- 完成したフックを返却
const queue: UpdateQueue<S, BasicStateAction<S>> = {
pending: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any),
};
laneはおなじみレーンのことかな
mountStateImplの調査を終わらせて、mountStateの調査に戻る
コード再掲
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];
}
今度はdispatchSetStateが気になるので見てみる
dispatchSetState関数
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);
}
実質的な処理はdispatchSetStateInternalに全部書いてありそう・・・
dispatchSetStateInternal関数
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;
}
見てみた感想
- まずupdateオブジェクトを作成
const update: Update<S, A> = {
lane,
revertLane: NoLane,
action,
hasEagerState: false,
eagerState: null,
next: (null: any),
};
これってこのupdateオブジェクトと同じか!
これも連結リストの構造になっているのは面白いかも
if (isRenderPhaseUpdate(fiber)) {
enqueueRenderPhaseUpdate(queue, update);
}
- isRenderPhaseUpdate関数でレンダリング中かどうかを判定
- レンダリング中の場合は更新キューに追加
enqueueRenderPhaseUpdate(queue, update);
- レンダリング中でないならenqueueConcurrentHookUpdateAndEagerlyBailout関数呼び出し?
これは何をしてるのかわからなかったが、今回は無視!
(balioutという処理を動かしているっぽい。ハック)
// 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?
- どっちみちenqueueConcurrentHookUpdate関数は実行される
- rootがnullでないならscheduleUpdateOnFiberとentangleTransitionUpdateを実行
const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
if (root !== null) {
scheduleUpdateOnFiber(root, fiber, lane);
entangleTransitionUpdate(root, queue, lane);
return true;
}
enqueueRenderPhaseUpdate関数を軽く確認
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;
}
- キューは基本的に循環リストにさせる
- pending = キューの一番新入りupdateオブジェクト キューなので一番最後尾
- pendingがnullなら (空っぽ)
- nextにはupdateを指定
- そうじゃないなら
- 最後尾に並ぶ
- updateオブジェクトのnextをpendingのnextとして指定
- 循環リストなのでpending.nextは最前列のupdateオブジェクト
- pendingのnextに今のupdateオブジェクトの参照をいれる
- キューのpendingを今のupdateオブジェクトであると更新
enqueueConcurrentHookUpdate関数
export function enqueueConcurrentHookUpdate<S, A>(
fiber: Fiber,
queue: HookQueue<S, A>,
update: HookUpdate<S, A>,
lane: Lane,
): FiberRoot | null {
const concurrentQueue: ConcurrentQueue = (queue: any);
const concurrentUpdate: ConcurrentUpdate = (update: any);
enqueueUpdate(fiber, concurrentQueue, concurrentUpdate, lane);
return getRootForUpdatedFiber(fiber);
}
enqueueUpdate関数のラッパー?
enqueueUpdate関数の詳細は以下を参照
ここでフックのupdateキューをFiberの持つupdateキューに追加しているということだろうか?
enqueueRenderPhaseUpdate関数は満足
mountState関数も満足
一通り見てきた感じがする
ここから気になること
- キューに入れられたupdateオブジェクトはどのように拾われるの?
- 初回マウント時しか見れてないけど更新時はどうなるんだろう
一気に解決していく
scheduleUpdateOnFiber関数で再レンダリングをスケジュールしているっぽい
ここからはreact scheduler側のコードになるっぽい
scheduleUpdateOnFiber関数は、トリガーを行うためのきっかけになる関数っぽい
ここ以降はまた別のスクラップで確認することに
また、renderWithHooks関数はどこから呼び出されるのか
→これはレンダーフェーズ編で解説する!