フックの仕組み
はじめに
React v16.8.0 で導入されたフックは、React アプリの書き方を変革しました。フック以前は、ステートやライフサイクルロジックが必要な場合、クラスコンポーネントを使用する必要がありました。フックの登場により、ファンクションコンポーネントが React アプリ開発のデファクトスタンダードとなりました。
フックは多くのことをシンプルにしました(クラスコンポーネントより優れていると言っているわけではありません)。コンポーネントの推論や扱いを容易にし、this
の扱いから解放されるなどの利点があります。
公式ドキュメントでフック自体の説明は十分されているため、ここでは React 内部での実装方法に焦点を当てます。
コンテキスト
前章の「ファンクションコンポーネントのレンダリング方法」で、コンポーネントが初回マウント時か更新時かに基づいてDispatcher
を設定することを説明しました。この Dispatcher の正体を解明しましょう。
ReactCurrentDispatcher
renderWithHooks
関数内で、ReactCurrentDispatcher.current
を設定します。これは React のすべてのフック実装を含むプレーンな JavaScript オブジェクトです。
Dispatcher オブジェクトの目的は、フックの使用を制限することにあります:
- レンダーフェーズ外でのフック使用を禁止(手動でファンクションコンポーネントを呼び出した場合など)
- マウント時と更新時で異なるフックの挙動(マウント時はフックの位置確保と初期化、更新時は更新ロジックの実行)
Dispatcher には React のフックに対応するプロパティが含まれます:
export const AllDispatchers: Dispatcher = {
readContext,
use,
useCallback: hook,
useContext: hook,
useEffect: hook,
useImperativeHandle: hook,
useInsertionEffect: hook,
useLayoutEffect: hook,
useMemo: hook,
useReducer: hook,
useRef: hook,
useState: hook,
useDebugValue: hook,
useDeferredValue: hook,
useTransition: hook,
useSyncExternalStore: hook,
useId: hook,
};
Dispatcher にはいくつかの種類がありますが、主に 4 つを解説します:
ContextOnlyDispatcher
: この Dispatcher はレンダーフェーズ外でのフック使用を防ぎます。いわゆる「Invalid hook call」エラーをスローしますHooksDispatcherOnMount
: この Dispatcher はコンポーネントが初回マウントされる際のフック実装を含みますHooksDispatcherOnUpdate
: この Dispatcher はコンポーネントが更新される際のフック実装を含みますHooksDispatcherOnRerender
: この Dispatcher は以下の場合の再レンダー時に使用されます:- レンダーフェーズ中に状態更新が発生した場合
- 開発モードでコンポーネントが 2 回レンダリングされる場合
フックのモデリング
各ファンクションコンポーネントはサポートされているフックを呼び出せます。すべてのフック呼び出しはrenderWithHooks
関数内で発生します(再レンダー用のフックはrenderWithHooksAgain
関数から呼び出される例外を除く)。
フックは関連するFiber
のmemoizedState
プロパティに保存されます。
フックは React 内部で以下のプロパティを持つプレーンオブジェクトとして保存されます:
const hook: Hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null,
};
各プロパティの役割:
memoizedState
: フックの「ステート」(または値)を保持baseState
: ステートフックが初期値を保存するために使用baseQueue
:queue
: ステートフックが様々な情報を保存するための UpdateQueue オブジェクトnext
: 次のフックを指す
next
プロパティがコンポーネントで使用する次のフックを参照することから、フックは前述のデータ構造のリンクリストとしてモデリングされています。
各フックはこれらのプロパティに何を保存するかについて独自の仕様を持ち、明らかにすべてのプロパティを使用しないフックもあります。
このデータ構造にはフックの種類に関する情報が含まれていないことに注目してください。フックは呼び出し順序に依存し、常に保存される必要があります。
Dan Abramov はこの設計選択について優れたブログ記事を書いています。
フックの例
以下のコンポーネントをレンダリングすると仮定します:
function MyFunctionComponent(props) {
const [count, setCount] = React.useState(0);
// デモ用のため実際には推奨されません
const isMounted = React.useRef(false);
// デモ用のため実際には推奨されません
const mountDate = React.useMemo(() => Date.now(), []);
React.useEffect(() => {
function handler() {
console.log("window is focused");
}
window.addEventListener("focus", handler);
return () => window.removeEventListener("focus", handler);
}, []);
return <span>Count is {count}</span>;
}
このコンポーネントをレンダリングすると、FunctionComponent
タグのFiber
が生成され、以下のようなフックのリンクリストが作成されます:
let memoizedState = {
// useState
memoizedState: 0,
baseState: 0,
baseQueue: null,
queue: {
pending: null,
lanes: 0,
lastRenderedState: 0,
},
next: {
// useRef
memoizedState: {
current: false,
},
baseState: null,
baseQueue: null,
queue: null,
next: {
// useMemo
memoizedState: [1700218172414, []],
baseState: null,
baseQueue: null,
queue: null,
next: {
// useEffect
memoizedState: {
tag: 9,
inst: {},
deps: [],
next: "the same effect .. removed for clarity",
},
baseState: null,
baseQueue: null,
queue: null,
next: null,
},
},
},
};
マウント時のフックの仕組み
マウント時のフックの目的は、リンクリストのフックの位置を確保することです。
そのため、すべてのフック実装はマウント時に以下の処理を最初に行います:
const hook = mountWorkInProgressHook();
mountWorkInProgressHook
関数は前述のデータ構造を作成し、それをcurrentlyRenderingFiber
のmemoizedState
プロパティに設定します。
mountWorkInProgressHook
の実装
マウント中のフック関数は以下のように実装されています:
function mountWorkInProgressHook(): Hook {
const hook: Hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null,
};
if (workInProgressHook === null) {
// これはリストの最初のフックです
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
// リストの末尾に追加
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
- 最初にフックオブジェクトを作成します
- その後、リストの最初のフックである場合、それを
currentlyRenderingFiber
のmemoizedState
にアタッチし、workInProgressHook
にも設定します - それ以外の場合、
workInProgressHook
のnext
プロパティにアタッチします
以上です!
フックによっては、他の処理も行われますが、それぞれのサポートされるフックについて別々に説明します。
更新時のフックの仕組み
コンポーネントが更新される(初回レンダリングではない)場合、各サポートされるフック呼び出しは以下の式で始まり、その後に特定の処理が続きます。
const hook = updateWorkInProgressHook();
updateWorkInProgressHook
はマウントよりも複雑ですが、目的は次のworkInProgressHook
を検出することです。これは更新と再レンダリングの両方に使用され、前回のレンダリングからcurrent
フックオブジェクトをクローンするか、work-in-progress
を再利用するかを想定しています。
この関数の最初の部分では、現在のレンダリングされたフックの値を検出します。currentHook
モジュール変数が null の場合、current
レンダリング済みファイバーのmemoizedState
プロパティをチェックします。それ以外の場合は、そのnext
プロパティを取得します:
// モジュールレベルで:
let currentHook: null | Hook = null;
// updateWorkInProgressHook内で:
let nextCurrentHook: null | Hook;
if (currentHook === null) {
// 現在のレンダリング済みファイバー
const current = currentlyRenderingFiber.alternate;
// すでにマウント済みの場合
if (current !== null) {
nextCurrentHook = current.memoizedState;
} else {
// 初回マウントの場合
nextCurrentHook = null;
}
} else {
nextCurrentHook = currentHook.next;
}
これで、現在のレンダリング済み(ペイント済み)フックの値を検出したので、React は次にその代替(レンダリング中のもの)を検出します:
// モジュールレベルで:
let workInProgressHook: null | Hook = null;
// updateWorkInProgressHook内で:
let nextWorkInProgressHook: null | Hook;
// リストの最初のフックの場合、レンダリング中のファイバーから取得
if (workInProgressHook === null) {
nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
} else {
// それ以外の場合、次のフック
nextWorkInProgressHook = workInProgressHook.next;
}
コンポーネントを更新する際、memoizedState プロパティはリセットされ、null に設定されます。
これで、現在のペイント済みフックの値と、レンダリング中のものを持つことができました。
nextWorkInProgressHook
がある場合、これはすでにレンダリングを開始していて、コミットやレンダリングの終了なしに再度レンダリングしていることを意味し、そのまま再利用します:
if (nextWorkInProgressHook !== null) {
// すでにワークインプログレスがあります。再利用します。
workInProgressHook = nextWorkInProgressHook;
nextWorkInProgressHook = workInProgressHook.next;
currentHook = nextCurrentHook;
}
それ以外の場合、nextCurrentHook
が null の場合、前回のレンダリングよりもフックを多くレンダリングしていることを意味し、フックの規則に反することになり、React はエラーをスローします。
nextCurrentHook
が null でない場合、前回のレンダリングのフックをクローンして基にする必要があります:
// Reactのコード
if (nextWorkInProgressHook !== null) {
// 前のコード
} else {
if (nextCurrentHook === null) {
const currentFiber = currentlyRenderingFiber.alternate;
if (currentFiber === null) {
// これは初回レンダリングです。このブランチは、コンポーネントが一時停止し、再開してから追加のフックをレンダリングするときに到達します。
// このブランチに到達することはありません。最初にマウントディスパッチャーに切り替える必要があります。
throw new Error(
"Update hook called on initial render. This is likely a bug in React. Please file an issue."
);
} else {
// これは更新です。常に現在のフックがあるはずです。
throw new Error("Rendered more hooks than during the previous render.");
}
}
currentHook = nextCurrentHook;
// 現在のペイント済みフックからクローン
const newHook: Hook = {
memoizedState: currentHook.memoizedState,
baseState: currentHook.baseState,
baseQueue: currentHook.baseQueue,
queue: currentHook.queue,
next: null,
};
if (workInProgressHook === null) {
// これはリストの最初のフックです。
currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
} else {
// リストの末尾に追加
workInProgressHook = workInProgressHook.next = newHook;
}
}
再レンダリング時のフックの仕組み
コンポーネントの再レンダリングという用語は、React コードベース内ではレンダーフェーズの更新をスケジュールしたか、開発モードで再生していることを意味します。
HooksDispatcherOnRerender
ディスパッチャーを見ると、useReducer: rerenderReducer
, useState: rerenderState
, useDeferredValue: rerenderDeferredValue
, useTransition: rerenderTransition
以外はHooksDispatcherOnUpdate
と同じであることがわかります。
このディスパッチャーはrenderWithHooksAgain
関数から設定されます。Andrew のコメントをコピーします:
// これは別のレンダーパスを実行するためのものです。レンダーフェーズの更新が呼び出されたとき、または開発モードでコンポーネントがレンダーフェーズで2回レンダリングされるときに使用されます。
//
// 前回のパスの状態は可能な限り再利用されます。そのため、すでに処理された状態更新は再度処理されず、メモ化された関数(`useMemo`)は再度呼び出されません。
//
// レンダーフェーズの更新がスケジュールされ続ける限り、ループでレンダリングを続けます。無限ループを防ぐためにカウンターを使用します。
各フックの仕組み
ディスパッチャーの存在順に従って、各フックを説明します。
use
フックの仕組み
use
フックは、データを待っている間にスローされるthrow promise
パターンを置き換える新しいフックです。
throw promise
は長くから存在していましたが、公式ではなく、このフックは公式の代替として導入されました。
シグネチャ
use
フックはここで定義されています。
function use<T>(usable: Usable<T>): T {
// [Not Native Code]
}
Promise または Context 型のオブジェクトを受け入れます。
use
フックはmountWorkInProgressHook
とupdateWIPHook
に依存しないため、条件付きで呼び出すことができ、フックの規則に従わなくなります。
実装
前述のように、use
はthenabled
とContext
の両方を受け入れます:
Context
use
に渡されたオブジェクトが React のContext
である場合、readContext
関数に処理を委譲します。これはuseContext
セクションで説明します。
if (
usable.$$typeof === REACT_CONTEXT_TYPE ||
usable.$$typeof === REACT_SERVER_CONTEXT_TYPE
) {
const context: ReactContext<T> = usable;
return readContext(context);
}
そのため、use
を使用すると条件付きでコンテキストにサブスクライブでき、フックの規則を回避できます 🤯
Thenable
Thenable オブジェクトが提供された場合、React は内部のuseThenable
関数を呼び出します:
function use<T>(usable: Usable<T>): T {
if (usable !== null && typeof usable === "object") {
if (typeof usable.then === "function") {
const thenable: Thenable<T> = usable;
return useThenable(thenable);
}
// ... other code
}
throw new Error("An unsupported type was passed to use(): " + String(usable));
}
useThenable
関数がuse
フックの仕事の背後にあることが明らかになりました。
初期化と thenable 状態のインクリメント(これについては説明しません)の後、useThenable
はtrackUsedThenable
を呼び出し、これがすべての仕事を行います。
function useThenable<T>(thenable: Thenable<T>): T {
// このファイバー内でthenableの位置を追跡します。
const index = thenableIndexCounter;
thenableIndexCounter += 1;
if (thenableState === null) {
// createThenableStateはプレーンなJavaScript配列を返します
thenableState = createThenableState();
}
const result = trackUsedThenable(thenableState, thenable, index);
// ... other code
return result;
}
それではtrackUsedThenable
について説明します:
パート 1:thenable を配列に追加する
ソフィーのコメントに注意してください。同じ位置に thenable があった場合、技術的には同じ値を指すはずですので、前者を再利用します。この設計選択について聞かれたら答えられません。
const previous = thenableState[index];
if (previous === undefined) {
thenableState.push(thenable);
} else {
if (previous !== thenable) {
// 前のthenableを再利用し、新しいものを捨てます。コンポーネントは恒等であると仮定できます。
// 意図的に無視するPromiseの未処理の拒否エラーを避けるために、それらを処理します。
thenable.then(noop, noop);
thenable = previous;
}
}パート 2:thenable を追跡する 前に thenable を追跡したことがあるか、初めて遭遇したかによって、2 つのケースがあります。
thenable を追跡することは、thenable 自体を変更する
then(onFullfilement, onRejection)
コールバックを追加することを意味します:以下のコードをよく読んで理解してください:
const pendingThenable: PendingThenable<T> = thenable;
pendingThenable.status = "pending";
pendingThenable.then(
(fulfilledValue) => {
if (thenable.status === "pending") {
const fulfilledThenable: FulfilledThenable<T> = thenable;
fulfilledThenable.status = "fulfilled";
fulfilledThenable.value = fulfilledValue;
}
},
(error: mixed) => {
if (thenable.status === "pending") {
const rejectedThenable: RejectedThenable<T> = thenable;
rejectedThenable.status = "rejected";
rejectedThenable.reason = error;
}
}
);しかし、thenable が既に追跡されている場合、単にその状態を確認します:
switch (thenable.status) {
case "fulfilled": {
const fulfilledValue: T = thenable.value;
return fulfilledValue;
}
case "rejected": {
const rejectedError = thenable.reason;
checkIfUseWrappedInAsyncCatch(rejectedError);
throw rejectedError;
}
// ... other code
}- 状態が
fulfilled
の場合、use
フックは値を返します - 状態が
rejected
の場合、use
フックはエラーをスローします
状態が
pending
の場合、React は特別な例外オブジェクトであるSuspenseException
をスローし、thenable が解決または拒否されるまでツリーを一時停止します。これにより、コンポーネントはデータがある場合にのみレンダリングされ、それ以外の場合はスローされます。
noteuse
フックは、エラーをキャッチするためにツリーにエラーバウンダリを配置する必要があります。- 状態が
use
フックは、手動でプロミスをキャッシュ/メモ化する必要があります。
React.cache
実験的な API は、これを助けることを目的としています。
例
jsonplaceholder の公開 API からユーザーの詳細を取得することを想定します。
これを実現するために、単純なキャッシュを作成して、プロミスをメモ化し、無限レンダリングを避けることができます。そこで、関数用のダミーメモライザーを作成します:
// キャッシュには1つのパラメータを使用すると仮定します
// ユーザーIDです。
// React.cacheはこれを解決する一般的なソリューションです。
// 明確にするため、userIdのみを使用します
function createCache(asyncFunc) {
let cache = {};
return function exec(...args) {
let cacheId = args[0];
let existing = cache[cacheId];
if (existing) {
return existing;
}
let result = asyncFunc.apply(null, args);
cache[cacheId] = result;
return result;
};
}
次に、ダミーエラーバウンダリを作成します:
class ErrorBoundary extends React.Component {
state = { error: null };
componentDidCatch(error) {
this.setState((prev) => ({ ...prev, error }));
}
render() {
const { error } = this.state;
if (error) {
return (
<>
<pre>{error.toString()}</pre>
<button
onClick={() => this.setState((prev) => ({ ...prev, error: null }))}
>
Reset
</button>
</>
);
}
return this.props.children;
}
}
最後に、このコードを利用します:
async function fetchUserById(userId) {
let result = await axios.get(
`https://jsonplaceholder.typicode.com/users/${userId}`
);
return result.data;
}
let getUserDetails = createCache(fetchUserById);
let IDS = [1, 2, 3, 4, 5, 10, 11];
function UserDetails({ id }) {
let details = React.use(getUserDetails(id));
return (
<details open>
<pre>{JSON.stringify(details, null, 4)}</pre>
</details>
);
}
function Example() {
let [userId, setUserId] = React.useState(IDS[0]);
return (
<div className="App">
{IDS.map((id) => (
<button
onClick={() => {
setUserId(id);
}}
key={id}
>
{`User ${id}`}
</button>
))}
<React.Suspense fallback={`Loading user ${userId}`}>
<UserDetails id={userId} />
</React.Suspense>
</div>
);
}
export default function App() {
return (
<ErrorBoundary>
<Example />
</ErrorBoundary>
);
}
このデモを表示して操作できます:
useCallback
フックの仕組み
useCallback
フックを使用すると、依存関係が変更されるまで関数参照を保持できます。
シグネチャ
useCallback
は以下のように定義されています:
function useCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
// [Not Native Code]
}
直接的には存在しない関数ですが、前述のようにmountCallback
とupdateCallback
関数があり、同じシグネチャを持ちます:
function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
// [Not Native Code]
}
function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
// [Not Native Code]
}
実装
マウント時
コンポーネントが最初にレンダリングされるときにuseCallback
を使用すると、呼び出しはmountCallback
によってインターセプトされ、最も簡単なフックです:
function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
// ステップ1
const hook = mountWorkInProgressHook();
// ステップ2
const nextDeps = deps === undefined ? null : deps;
// ステップ3
hook.memoizedState = [callback, nextDeps];
return callback;
}
- ステップ 1:前述のフックデータ構造をマウントします
- ステップ 2:依存関係を定義します。パラメータが省略された場合は
null
を使用します - ステップ 3:コールバックと依存関係をフックの
memoizedState
プロパティに格納します
useCallback
は渡された値をそのまま返します。通常、インライン関数を直接定義するか、コンポーネント本体内で定義された関数を渡します。
マウント時にuseCallback
は依存関係については関心がありません。単にそれらを後で使用するために格納するだけです。
更新時
更新時、目的は依存関係が変更された場合にのみ新しい関数参照を与えることです。
function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
// ステップ1
const hook = updateWorkInProgressHook();
// ステップ2
const nextDeps = deps === undefined ? null : deps;
const prevState = hook.memoizedState;
// ステップ3
if (nextDeps !== null) {
const prevDeps: Array<mixed> | null = prevState[1];
// ステップ4
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
// ステップ5
hook.memoizedState = [callback, nextDeps];
return callback;
}
- ステップ 1:フックデータ構造オブジェクトを作成または再利用します
- ステップ 2:依存関係配列を推論します。指定されていない場合は
null
を使用します - ステップ 3:依存関係が null でない場合(undefined を指定するとメモ化しないことを意味します)、前の依存関係と比較します
- ステップ 4:前の依存関係と次の依存関係を比較し、同じ場合は前の値(
memoizedState
配列の最初の要素)を返します。比較方法は後で説明します - ステップ 5:依存関係が変更されたか、メモ化していない場合、
mountCallback
と同様に[callback, nextDeps]
をフックのmemoizedState
プロパティに格納します
The areHookInputsEqual
関数は依存配列を使用するすべてのフックで使用されます。この関数は:
- 前回の依存配列がない場合、常に
false
を返します。これにより React はフックの返り値を再計算します。つまり、依存配列が空([]
)の場合、毎回のレンダーで更新されます - 両方の配列をループし、
Object.is
で個々の値を比較します
useContext フックの仕組み
useContext
フックはReact Contextの値を読み取り、変更をサブスクライブすることを可能にします。
シグネチャ
useContext
フックは次のように定義されます:
function readContext<T>(context: ReactContext<T>): T {
// [Not Native Code]
}
ここでパラメータはReact.createContext
APIで作成された React Context オブジェクトを指します。
実装
useContext
はreadContextForConsumer
関数を使用します:
export function readContext<T>(context: ReactContext<T>): T {
// ...開発環境用のチェック
return readContextForConsumer(currentlyRenderingFiber, context);
}
readContextForConsumer
関数は現在の Context 値を取得し、将来の変更をサブスクライブする役割を担います。実装の詳細を見ていきましょう:
function readContextForConsumer<T>(
consumer: Fiber | null,
context: ReactContext<T>
): T {
// ステップ1
const value = isPrimaryRenderer
? context._currentValue
: context._currentValue2;
// ステップ2
if (lastFullyObservedContext === context) {
// 既にこのContextを完全に監視している場合は何もしない
// ステップ3
} else {
}
}
- ステップ 1:
isPrimaryRenderer
プロパティに基づいて内部の Context 値を決定します。このプロパティはカスタム React レンダラー作成時に設定されます。Primary レンダラーはページ全体をレンダーし、Secondary は他のレンダラーの上で使用されることを意味します。React-DOM の場合、Primary レンダラーとして動作するため_currentValue
を使用します - ステップ 2:
lastFullyObservedContext
モジュール変数を使用して Context の監視状態をチェックします。この変数はコードベース全体で未使用のように見えます - ステップ 3: 実際のサブスクリプション処理が行われる部分(後述)
Context サブスクリプションの仕組み
Context のサブスクリプションはfiber.dependencies
プロパティにリンクリスト形式で保存されます:
// 簡略化版
function readContextForConsumer<T>(
consumer: Fiber | null,
context: ReactContext<T>
): T {
const value = context._currentValue;
const contextItem = {
context: context as ReactContext<any>,
memoizedValue: value,
next: null,
};
}
コンポーネントで初めてuseContext
が呼ばれる場合、以下のオブジェクトをdependencies
として追加します:
// 簡略化版
// updateFunctionComponentなどコンポーネント更新時に
// prepareToReadContext関数でlastContextDependency変数をリセット
if (lastContextDependency === null) {
lastContextDependency = contextItem;
// consumerは現在処理中のfiber
consumer.dependencies = {
lanes: NoLanes,
firstContext: contextItem,
};
}
既存の依存関係がある場合、context アイテムを前のアイテムのnext
プロパティに追加します:
if (lastContextDependency === null) {
// ...
} else {
lastContextDependency = lastContextDependency.next = contextItem;
}
And that's it!
更新処理
ContextProvider
をレンダリングするコンポーネントが更新されると、React はvalue
プロパティをチェックします。値が変更された場合、変更を伝播します。
このセクションの詳細は、ContextProvider
のレンダリングの仕組みで説明されます。
use
フックと同様に、useContext
はレンダー中に条件付きで呼び出すことが可能です。
ただし、他のフック内やレンダーフェーズ外で呼び出すことはできません。現在レンダリング中の fiber が必要なため、サブスクリプションを実行できないからです。
useEffect フックの仕組み
useEffect
フックはコンポーネントに passive effects を登録することを可能にします。
Passive effects はレンダーの commit フェーズの最後の部分で実行されます。SyncLane
の場合は同期的に、その他の lane の場合は非同期に実行されます。
公式ドキュメントより:
useEffect はコンポーネントを外部システムと同期させる React フックです
これは、ブラウザ API(フォーカス、リサイズ、ブラーなど)や外部ストアとの同期にのみ使用すべきであることを意味します。
シグネチャ
useEffect
フックは以下のように定義されます:
function useEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null
): void {
// [Not Native Code]
}
パラメータ:
create
: effect 作成関数。パラメータを受け取らず、何も返さないかcleanup
関数を返します。クリーンアップ関数はイベントの購読解除など、effect の後処理を担当します。deps
: オプションの依存配列。依存関係が変更されるたびに effect の作成が再実行されます。このパラメータが省略された場合、effect はすべてのレンダーフェーズの終了時に実行されます。
レンダーフェーズ中に状態更新が発生しても、effect は 2 回実行されません。effect は commit フェーズで実行されます。
マウント時の実装
通常のフックと同様、mountWorkInProgressHook()
を使用します。mountEffect
はmountEffectImpl
関数を呼び出します。
mountEffectImpl
は他の effect フック(useLayoutEffect、useInsertionEffect など)からも呼び出されます。
function mountEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null
): void {
mountEffectImpl(
PassiveEffect | PassiveStaticEffect,
HookPassive,
create,
deps
);
}
mountEffectImpl
のシグネチャを確認しましょう:
function mountEffectImpl(
fiberFlags: Flags,
hookFlags: HookFlags,
create: () => (() => void) | void,
deps: Array<mixed> | void | null
): void {
// [Not Native Code]
}
fiberFlags
: effect を使用するコンポーネントに追加されるフラグhookFlags
: effect 自体を定義するフラグ。可能な値はInsertion
、Layout
、Passive
。useEffect
ではPassive
が使用されますcreate
: effect 関数deps
: effect の依存関係
mountEffectImpl
関数の実装を見てみましょう:
function mountEffectImpl(
fiberFlags: Flags,
hookFlags: HookFlags,
create: () => (() => void) | void,
deps: Array<mixed> | void | null
): void {
// ステップ1
const hook = mountWorkInProgressHook();
// ステップ2
const nextDeps = deps === undefined ? null : deps;
// ステップ3
currentlyRenderingFiber.flags |= fiberFlags;
// ステップ4
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
createEffectInstance(),
nextDeps
);
}
- ステップ 1: フックデータ構造をマウントします
- ステップ 2: 依存関係を定義します。パラメータが省略された場合は
null
を使用します - ステップ 3: 受け取った fiberFlags を現在レンダリング中の fiber に追加します。
useEffect
の場合、PassiveEffect | PassiveStaticEffect
(執筆時点では自然数8390656
)が使用されます - ステップ 4: フックの
memoizedState
値としてpushEffect
の結果を保存します
createEffectInstance
関数は{ destroy: undefined }
オブジェクトを返します。これは effect のクリーンアップ関数(存在する場合)を保存するために使用されます
最後にpushEffect
関数を見てみましょう:
function pushEffect(
tag: HookFlags, // useEffect: Passive
create: () => (() => void) | void,
inst: EffectInstance, // { destroy: undefined }
deps: Array<mixed> | null
): Effect {
// [Not Native Code]
}
effect オブジェクトの作成
このオブジェクトはレンダーごとに各 effect 用に作成され、必要な情報を保持します:
const effect: Effect = {
tag, // フックフラグ
create, // 提供されたeffect関数
inst, // { destroy: undefined }
deps, // 依存関係またはnull
// 循環参照
next: null, // 後で設定されます
};
effect を関数コンポーネントの update queue にリンク
次に、React はcurrentlyRenderingFiber.updateQueue
プロパティを参照します。null の場合、初期化します:
let componentUpdateQueue: null | FunctionComponentUpdateQueue =
currentlyRenderingFiber.updateQueue;
if (componentUpdateQueue === null) {
componentUpdateQueue = createFunctionComponentUpdateQueue();
currentlyRenderingFiber.updateQueue = componentUpdateQueue;
// 循環参照を作成(コミット時に分解されます)
componentUpdateQueue.lastEffect = effect.next = effect;
}
createFunctionComponentUpdateQueue
で作成される update queue は以下のようになります:
const updateQueue = {
lastEffect: null,
events: null,
stores: null,
};
// memoCache機能が有効な場合、nullで初期化されたmemoCacheプロパティが追加されます
これは循環リンクリストとして使用されます。lastEffect を保存すると、そのnext
プロパティはリストの最初の effect を指すようになります。
コンポーネントのupdateQueue
が既に初期化されている場合(このレンダーで以前に effect を呼び出したか、他のフックが初期化した場合)、React はlastEffect
プロパティを取得し:
null
の場合(updateQueue が effect 以外のイベントやストアで初期化された可能性あり)、以前と同じ処理を行います:effect
オブジェクトと自身の循環参照を作成し、キューのlastEffect
プロパティに保存します。const lastEffect = componentUpdateQueue.lastEffect;
if (lastEffect === null) {
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
// 次の処理を見てみましょう
}null
でない場合、このレンダーパスで以前に effect フックを呼び出したことを意味し、以下のコードを実行します:混乱しないように、コードを分解してみましょう:const firstEffect = lastEffect.next;
lastEffect.next = effect;
effect.next = firstEffect;
componentUpdateQueue.lastEffect = effect;- まずリストの最初の effect を参照します(循環リンクリストのため、最初の要素は最後の要素の
next
です) - 新しい effect を前の
lastEffect
のnext
として追加します(これが新しい最後の要素になります) - 新しい effect(新しい最後の要素)は
next
プロパティでfirstEffect
を指すようになります - 最後に、新しい effect をコンポーネントの
updateQueue
リストの lastEffect としてマークします
- まずリストの最初の effect を参照します(循環リンクリストのため、最初の要素は最後の要素の
最終的に、pushEffect
関数は新しく作成された effect オブジェクトを返し、hook.memoizedState
に保存します。
Implementation on update
更新時の実装
更新時、useEffect
はHooksDispatcherOnUpdate
ディスパッチャーからupdateEffect
を呼び出し、updateEffectImpl
関数に委譲します。
まず、updatewWorkInProgressHook
関数が呼ばれます。
function updateEffectImpl(
fiberFlags: Flags, // useEffectの場合PassiveEffect
hookFlags: HookFlags, // useEffectの場合HookPassive
create: () => (() => void) | void,
deps: Array<mixed> | void | null
): void {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
// 前回のレンダーからのeffect
const effect: Effect = hook.memoizedState;
// 前回のレンダーからのeffectインスタンス(再利用される)
const inst = effect.inst;
// currentHookは、レンダーフェーズ後の再レンダー時やStrict Modeでの初期マウント時にnullになります
// updateWIPHookセクションでcurrentHookについて既に説明しています
if (currentHook !== null) {
if (nextDeps !== null) {
const prevEffect: Effect = currentHook.memoizedState;
const prevDeps = prevEffect.deps;
if (areHookInputsEqual(nextDeps, prevDeps)) {
// マウント時と同様のpushEffectを呼び出し
hook.memoizedState = pushEffect(hookFlags, create, inst, nextDeps);
return;
}
}
}
// fiberフラグにfiberFlagsを追加
currentlyRenderingFiber.flags |= fiberFlags;
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
inst,
nextDeps
);
}
これが useEffect の動作原理です。effect 関数は commit フェーズで実行されます。レンダー中は関連情報を保存するだけです。
各タイプの effect の実行タイミングについて詳しくは、how commit works
セクションを参照してください。
How useImperativeHandle works
useImperativeHandle
フックは公式ドキュメントで次のように定義されています:
useImperativeHandle は、ref として公開されるハンドルをカスタマイズできる React フックです
具体的には、カスタムコンポーネントが公開する ref(ハンドル)をオーバーライドできます。例えば、カスタムボタンにsayHiTo(name)
関数を追加してアラートを表示する場合などに使用します。
シグネチャ
useImperativeHandle
は以下のように定義されます:
function mountImperativeHandle<T>(
ref: { current: T | null } | ((inst: T | null) => mixed) | null | void,
create: () => T,
deps: Array<mixed> | void | null
): void {
// [Not Native Code]
}
- ref:
useRef
またはcreateRef
で作成された ref オブジェクト、または ref コールバック - create: 新しい ref ハンドルを返す関数
- deps: フックの依存関係。依存関係が変更されると create 関数が再呼び出しされます
マウント時の実装
useImperativeHandle
を使用するコンポーネントが初めてレンダリングされるとき、mountImperativeHandle
関数が呼び出されます:
// step 1
const effectDeps =
deps !== null && deps !== undefined ? deps.concat([ref]) : null;
// step 2
mountEffectImpl(
UpdateEffect | LayoutStaticEffect,
HookLayout,
imperativeHandleEffect.bind(null, create, ref),
effectDeps
);
- ステップ 1: 実際のフック依存関係を計算します。提供された依存関係に ref オブジェクト自体を追加します。ref を配列に追加せず、開発者が手動で追加するべきと仮定することも可能ですが、後方互換性が失われます
- ステップ 2: 2 番目のステップは effect をマウントすることです(驚きですね!😳)
その通りです。useImperativeHandle
は特別な layout effect を挿入します。この effect のcreate
関数はimperativeHandleEffect
関数です
コミットフェーズ中、React はLayout
フェーズで ref をアタッチします。これが全体の処理が layout effect として使用される理由です
更新時の実装
更新時、useImperativeHandle
はマウント時と同様に依存関係を計算し、UpdateEffect
を fiber フラグとしてupdateEffectImpl
を呼び出します
これが実際の処理内容です。
imperativeHandleEffect 関数
シグネチャ
function imperativeHandleEffect<T>(
create: () => T,
ref: { current: T | null } | ((inst: T | null) => mixed) | null | void
): void | (() => void) {
// [Not Native Code]
}
Implementation
渡された ref が ref オブジェクトか ref コールバックかに基づいて処理を実行し、いずれの場合も渡されたcreate
関数を呼び出し、layout effect 用のクリーンアップ関数を返します:
if (typeof ref === "function") {
// step 1
const refCallback = ref;
// step 2
const inst = create();
// step 3
refCallback(inst);
// step 4
return () => {
refCallback(null);
};
}
- Step 1: 渡された ref コールバックの参照を保持
- Step 2: useImperativeHandle の create 関数を呼び出し、新しい ref ハンドルを生成
- Step 3: 生成されたハンドルで ref コールバックを呼び出し
- Step 4: ref コールバックを null で再度呼び出すクリーンアップ関数を返却
別のケースとして、渡されたref
が ref オブジェクトの場合、imperativeHandleEffect
は以下を実行します:
// 元々はelse if節
if (ref !== null && ref !== undefined) {
// step 1
const refObject = ref;
// step 2
const inst = create();
// step 3
refObject.current = inst;
// step 4
return () => {
refObject.current = null;
};
}
- Step 1: 渡された ref オブジェクトの参照を保持
- Step 2: useImperativeHandle の create 関数を呼び出し、新しい ref ハンドルを生成
- Step 3: 生成された ref ハンドルを ref オブジェクトの
current
プロパティに割り当て - Step 4:
current
プロパティを null にリセットする layout effect のクリーンアップ関数を返却
これで完了です!
前述の通り、imperativeHandleEffect
はコミットフェーズの layout effect イテレーション中に呼び出されます。レンダー中に即座に呼び出されることはありません。
How useInsertionEffect works
useInsertionEffect
フックは公式ドキュメントで次のように定義されています:
すべての layout effects が発火する前に DOM 要素を挿入できるようにします
公式ドキュメントに記載されている通り、css-in-js ライブラリの作者のみが使用すべきフックです。それ以外の場合はuseLayoutEffect
またはuseEffect
を使用してください。
シグネチャ
他の effects と同様、useInsertionEffect
は以下のように定義されます:
function useInsertionEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null
): void {
// [Not Native Code]
}
実装
useInsertionEffect
の実装はuseEffect
と同じです。唯一の違いは、マウント時と更新時にmountEffectImpl
とupdateEffectImpl
に渡されるflags
です:
- マウント時:React は fiber flags として
UpdateEffect
、hook flags としてHookInsertion
を渡します - 更新時:React は fiber flags として
UpdateEffect
、hook flags としてHookInsertion
を渡します
これだけです!すべての effects は flags の違いのみで区別されます。
How useLayoutEffect works
useLayoutEffect
フックは公式ドキュメントで次のように定義されています:
ブラウザが画面を再描画する前に発火する useEffect のバージョンです
ただし、useEffect との比較においてこれは完全に正確ではありません。how commit works
セクションで詳細が明らかになります。
useLayoutEffect
は、DOM 要素の変更後に同期的に実行される effect を登録できます。
その同期的な性質により、ブラウザのメインスレッドをブロックし、新しい UI の部分的なペイントを防ぎます。これが「useLayoutEffect はブラウザのペイント前に実行される」と言われる理由です。
useLayoutEffect
はClassComponent
のライフサイクルメソッド(componentDidMount
とcomponentDidUpdate
)と同じタイミングで実行されます。
シグネチャ
他の effects と同様、useInsertionEffect
は以下のように定義されます:
function useLayoutEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null
): void {
// [Not Native Code]
}
実装
useLayoutEffect
の実装はuseEffect
と同じです。唯一の違いは、マウント時と更新時にmountEffectImpl
とupdateEffectImpl
に渡されるflags
です:
- マウント時:React は fiber flags として
UpdateEffect | LayoutStaticEffect
、hook flags としてHookLayout
を渡します - 更新時:React は fiber flags として
UpdateEffect
、hook flags としてHookLayout
を渡します
これだけです!すべての effects は flags の違いのみで区別されます。
useMemo の仕組み
useMemo
フックは依存関係が変更されるまで値をキャッシュします。
シグネチャ
useMemo
は以下のように定義されます:
function useMemo<T>(nextCreate: () => T, deps: Array<mixed> | void | null): T {
// [Not Native Code]
}
パラメータ:
nextCreate
: キャッシュする値を生成する関数deps
: 依存関係
マウント時の実装
マウント時、useMemo
はmountMemo
を呼び出します。実装は以下の通り:
function mountMemo<T>(
nextCreate: () => T,
deps: Array<mixed> | void | null
): T {
// ステップ1
const hook = mountWorkInProgressHook();
// ステップ2
const nextDeps = deps === undefined ? null : deps;
// ステップ3
const nextValue = nextCreate();
// ステップ4
hook.memoizedState = [nextValue, nextDeps];
// ステップ5
return nextValue;
}
- ステップ 1: マウント時に hook オブジェクトを作成
- ステップ 2: 提供された依存関係または
null
を使用 - ステップ 3: 初期メモ値を計算
- ステップ 4:
[nextvalue, nextDeps]
を hook のmemoizedState
に保存 - ステップ 5: キャッシュされた値を返却
開発モードで StrictMode が有効な場合、React はnextCreate
を 2 回呼び出します:
// renderWithHooks関数で初期化
if (shouldDoubleInvokeUserFnsInHooksDEV) {
nextCreate();
}
アップデート時の実装
アップデート時、useMemo
はupdateMemo
を呼び出します。実装は以下の通り:
function updateMemo<T>(
nextCreate: () => T,
deps: Array<mixed> | void | null
): T {
// ステップ1
const hook = updateWorkInProgressHook();
// ステップ2
const nextDeps = deps === undefined ? null : deps;
// ステップ3
const prevState = hook.memoizedState;
// 定義されていると仮定。未定義の場合、areHookInputsEqualが警告を出します
if (nextDeps !== null) {
const prevDeps: Array<mixed> | null = prevState[1];
// ステップ4
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
if (shouldDoubleInvokeUserFnsInHooksDEV) {
nextCreate();
}
// ステップ5
const nextValue = nextCreate();
// ステップ6
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
- ステップ 1: アップデート用の hook オブジェクトを作成(または未完了レンダーから再利用)
- ステップ 2: 依存関係を計算(提供されたものか
null
) - ステップ 3: 前回レンダーされた値を参照(レンダーが未完了でも、実際の値を使用)
- ステップ 4: 依存関係が同じ場合、前回キャッシュされた値を返却
- ステップ 5: 渡された
nextCreate
メモ関数で新しい値を計算 - ステップ 6:
[nextValue, nextDeps]
を hook のmemoizedState
に保存し、新しいキャッシュ値を返却
これで完了です!useMemo
の実装は複雑さの面でuseCallback
と似ていますが、違いはuseMemo
が関数を実行するのに対し、useCallback
は関数そのものを返す点です。
useReducer の仕組み
useReducer
フックはコンポーネントに reducer を追加できます。
reducer
とは、現在のvalue
と適用するaction
を受け取り、new value
を返す関数です。例:
function reducer(prevValue, action) {
// actionとvalueに基づいたロジックを実行
// ...
// 新しいvalueを返す
}
シグネチャ
useReducer
フックは以下のように定義されます:
function useReducer<S, I, A>(
reducer: (state: S, action: A) => S,
initialArg: I,
init?: (initialValue: I) => S
): [S, Dispatch<A>] {
// [Not Native Code]
}
パラメータ:
- reducer: reducer 関数
- initialArg: 初期値
- init:
initialArg
を受け取るオプションの初期化関数
返り値は 2 要素の配列:
- state: 状態値
- dispatch: action を reducer に渡す関数
マウント時の実装
useReducer
を使用するコンポーネントが初めてレンダリングされるとき、mountReducer
関数が呼び出されます:
work in progress hook をマウント:
予想通り、最初に行うのはこれです。理由がわからない場合は、このセクション全体をスキップした可能性があります 😑
const hook = mountWorkInProgressHook();
初期 state を計算: 初期 state の計算は、第三引数を指定したかどうかに依存します:
let initialState;
if (init !== undefined) {
initialState = init(initialArg);
} else {
initialState = initialArg as S;
}initialArg
そのものか、それに関数が適用された結果のいずれかになります。hook の
memoizedState
プロパティを割り当て:hook.memoizedState = hook.baseState = initialState;
UpdateQueue オブジェクトを作成して割り当て:
update queue は
useReducer
の内部状態を参照するオブジェクトです。ここでは詳細に触れませんが、主にコンポーネントのアップデート時や React が状態更新をスケジューリング/処理する際に使用されます。その構造と参照要素を確認しましょう。初期状態では、最初に与えられた
reducer
と計算されたinitialState
のみを参照します:const queue: UpdateQueue<S, A> = {
pending: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: reducer,
lastRenderedState: initialState,
};
hook.queue = queue;dispatch 関数を作成して返却:
const dispatch: Dispatch<A> = (queue.dispatch = dispatchReducerAction.bind(
null,
currentlyRenderingFiber,
queue
));
return [hook.memoizedState, dispatch];dispatch 関数は非常に重要で、
mountReducer
は必要なパラメータのうち 2 つをバインドして渡します。詳細を見ていきましょう。
How dispatchReducerAction
works
dispatchReducerAction
は以下のように定義されます:
function dispatchReducerAction<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A
): void {
// [Not Native Code]
}
この関数の役割は、指定されたaction
を使用して、対象のfiber
とそのqueue
(1 つのコンポーネントが複数のuseReducer
を持つ可能性があり、キューは処理待ちのアップデートを保持)にアップデートをスケジュールすることです。
言い換えれば、これはuseReducer
やuseState
が提供するdispatch
関数の実体です。
これで、関数コンポーネントにおける state 更新関数の仕組みを理解できます。
この部分を理解するには、how root.render() works
セクションを事前に読むと理解が容易です。重複を避けるため、スケジューリングの基本的な仕組みは同じであることを前提に説明します。
アップデート lane を要求
const lane = requestUpdateLane(fiber);
最初のステップは、アップデートに使用する
lane
を決定します。例:- アプリケーションがコンカレントモード(
createRoot().render()
)でレンダリングされていない場合、SyncLane
が使用される - レンダーフェーズ中のアップデートの場合、最高優先度の lane が使用される(または指定された Lanes 番号内で最小の lane)
startTransition
でラップされた場合、トランジション lane の 1 つ- root レンダースケジュールの仕組みで説明したイベント優先度
- アプリケーションがコンカレントモード(
アップデートオブジェクトを作成
const update: Update<S, A> = {
lane,
revertLane: NoLane,
action,
hasEagerState: false,
eagerState: null,
next: null,
};アップデートオブジェクトは複数のプロパティを持ちますが、ここで重要なのは
lane
とaction
です。React は後で処理できるようこれらの情報を保持します。next
プロパティはリンクリストを形成するために使用されます。レンダーフェーズ中に呼び出された場合、レンダーフェーズアップデートをキューに追加
if (isRenderPhaseUpdate(fiber)) {
enqueueRenderPhaseUpdate(queue, update);
}レンダーフェーズアップデートは、
fiber
がcurrentlyRenderingFiber
またはその alternate と等しい場合に検出されます。この場合、React は
enqueueRenderPhaseUpdate
関数を呼び出します。実装は以下の通りです:function enqueueRenderPhaseUpdate<S, A>(
queue: UpdateQueue<S, A>,
update: Update<S, A>
): void {
// ステップ1
didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate =
true;
const pending = queue.pending;
if (pending === null) {
// ステップ2
update.next = update;
} else {
// ステップ3
update.next = pending.next;
pending.next = update;
}
// ステップ4
queue.pending = update;
}- ステップ 1: レンダーフェーズアップデートがスケジュールされたことをマーク。この変数は、React がコミット前にコンポーネントを再レンダーする必要があることを判断するために重要
- ステップ 2: キューに保留中のアップデートがない場合、アップデートは自身を
next
として指す - ステップ 3: 保留中のアップデートが存在する場合(循環リンクリスト構造)、新しいアップデートの
next
を最初のアップデートに設定し、前の最後のアップデートのnext
を新しいアップデートに設定 - ステップ 4: 新しいアップデートをキューの
pending
(最後のアップデート)としてマーク
レンダーフェーズ外で呼び出された場合、コンカレントフックアップデートをエンキューし、fiber のアップデートをスケジュールし、トランジションを entangle
if (false) {
} else {
const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
if (root !== null) {
scheduleUpdateOnFiber(root, fiber, lane);
entangleTransitionUpdate(root, queue, lane);
}
}この処理は
how root.render() works
で説明したものと同じです。最終的には、アップデートを引き起こした fiber の updateQueue を処理するためのマイクロタスクがスケジュールされます。
React のアップデートスケジューリングは効率的なプロセスです。子 fiber のflags
を親に集約することで、作業の開始と停止タイミングを正確に判断できます。
重要なポイント:
- React はアップデートを即時処理せず、キューに追加して後でスケジュールします
- アップデートの詳細(キュー、アクション/値/アップデータ)を追跡し、後処理可能に保持
- アップデートをキューイングすることでバッチ処理が可能になり、複数回ではなく 1 回のレンダーで済ませられます
アップデート時の実装
This section is not available yet. Please fill an issue.
While waiting for the newsletter, you can get notified when new content drops by following me on X.
How useRef works
useRef
フックは、自由に制御可能な参照を提供します。任意の JavaScript の値を参照できます。
公式ドキュメントでは次のように説明されています:
useRef はレンダリングに不要な値を参照できる React フックです
React は、コンポーネントのレンダリング中にこの ref を使用して判断を下すことを推奨していません。ただし、レンダー外での操作は問題ありません。
例えば、レンダー中にこの値を変更すると、React は複数回レンダーする可能性があり、その結果複数回書き込まれることになります。これを確認する最も簡単な方法は、コンポーネントのレンダー回数をカウントするために ref を使用することです。開発モードや Strict Mode では常に誤った結果が得られます。この種の使用法から離れるべき理由の 1 つです。
また、コンポーネントが現在マウント中かどうかを検出するためによく ref が使用されますが、これも誤りです。コンポーネントが既にマウントされ、Suspense フォールバックを表示しながらデータ待ちで中断した場合、コンポーネントはマウントされたままなので、この値に基づく判断はすべて誤りになります。
この ref オブジェクトを HTML 要素に渡すと、React はレイアウトエフェクト時に実際の DOM 要素をアタッチします。これもまた、レンダー中にこのフックを使用せず、使用を最小限に抑えるべき理由です。
useRef
フックの適切な使用法については、公式ドキュメントを参照してください。
Signature
The useRef
hook is defined as follows:
function mountRef<T>(initialValue: T): { current: T } {
// [Not Native Code]
}
マウント時の実装
コンポーネントが初めてuseRef
を使用してレンダーする際、mountRef
関数が呼び出されます。これは最もシンプルなフック実装の 1 つです:
// 簡略化版:レンダー中のref参照に関する開発警告は削除
function mountRef<T>(initialValue: T): { current: T } {
// ステップ1
const hook = mountWorkInProgressHook();
// ステップ2
const ref = { current: initialValue };
// ステップ3
hook.memoizedState = ref;
// ステップ4
return ref;
}
- ステップ 1: マウント時にフックオブジェクトを作成
- ステップ 2:
useRef
が受け取った初期値initialValue
でcurrent
プロパティを持つ JavaScript オブジェクトを生成 - ステップ 3: このオブジェクトをフックの
memoizedState
に保存 - ステップ 4: ref オブジェクトを返却
アップデート時の実装
アップデート時、useRef
はupdateRef
関数を呼び出します。この実装は非常にシンプルで追加の説明は不要です:
function updateRef<T>(initialValue: T): { current: T } {
const hook = updateWorkInProgressHook();
return hook.memoizedState;
}
このコードはフックオブジェクトを作成しながら ref のミュータブルなオブジェクトをそのまま再利用し、memoized state を直接返却します。
How useState works
useState
フックはコンポーネントに state 変数を追加し、値が変更された際に再レンダーをトリガーします。
公式ドキュメントより:
useState はコンポーネントに state 変数を追加する React フックです
現在 React において、state は唯一のリアクティブプリミティブです(Promise も内部的にはscheduleUpdateOnFiber
を呼び出しますが、それらだけで動作させるのは困難です)。
このセクションの理解には、useReducer
セクションの内容を前提とします。
シグネチャ
useState
は以下のように定義されます:
function useState<S>(
initialState: (() => S) | S
): [S, Dispatch<BasicStateAction<S>>] {
// [Not Native Code]
}
パラメータ:
- initialState: 初期状態値または初期状態を生成する関数
マウント時の実装
useState
は内部的にuseReducer
を使用して実装されています。具体的には以下の reducer 関数を使用します:
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
return typeof action === "function" ? action(state) : action;
}
このように、関数を受け取った場合は現在の state を引数にその関数を呼び出し、そうでない場合は渡された値をそのまま使用する reducer を useReducer
に渡すことで、私たちが知っている useState
のように振る舞わせることができます。
function useState<S>(
initialState: (() => S) | S
): [S, Dispatch<BasicStateAction<S>>] {
// ステップ1
const hook = mountStateImpl(initialState);
const queue = hook.queue;
// ステップ2
const dispatch: Dispatch<BasicStateAction<S>> = dispatchSetState.bind(
null,
currentlyRenderingFiber,
queue
);
// ステップ3
queue.dispatch = dispatch;
return [hook.memoizedState, dispatch];
}
- ステップ 1:
mountWIPHook
を呼び出し、hook オブジェクトとUpdateQueue
を初期化 - ステップ 2:
dispatch
関数作成時にキューを参照 - ステップ 3: state の現在値と
dispatch
関数(setState
)を含むタプルを返却
mountStateImpl
の動作
この関数の実装を見てみましょう:
function mountStateImpl<S>(initialState: (() => S) | S): Hook {
// ステップ1
const hook = mountWorkInProgressHook();
// ステップ2
if (typeof initialState === 'function') {
initialState = initialState();
}
// ステップ3
hook.memoizedState = hook.baseState = initialState;
// ステップ4
const queue: UpdateQueue<S, BasicStateAction<S>> = {
pending: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any),
};
hook.queue = queue;
return hook;
}
- ステップ 1: hooks のリンクリスト内に hook オブジェクトをマウント
- ステップ 2:
initialState
が関数の場合、初期化関数を実行 - ステップ 3: hook の
memoizedState
とbaseState
に初期値を設定 - ステップ 4: update queue オブジェクトを作成し hook の queue に割り当て
これで完了です!useState は useReducer です。2018 年に hooks が導入されたコミットを見ると、実際に以下のように実装されていました:
export function useState<S>(
initialState: S | (() => S)
): [S, Dispatch<S, BasicStateAction<S>>] {
return useReducer(basicStateReducer, initialState);
}
Implementation on update
On updates, updateState
will delegate the work entirely to updateReducer
,
and that's it!
function updateState<S>(
initialState: (() => S) | S
): [S, Dispatch<BasicStateAction<S>>] {
return updateReducer(basicStateReducer, initialState);
}
How useDebugValue works
これは開発モードでのみ機能する空のフックです。
useDebugValue
の実装はmountDebugValue
で示される通り空です:
function mountDebugValue<T>(value: T, formatterFn?: (value: T) => mixed): void {
// 通常このフックは何もしません
// react-debug-hooksパッケージが独自実装を注入し、
// DevToolsでカスタムフックの値を表示できるようにします
}
const updateDebugValue = mountDebugValue;
ReactDebugHooks.js
ファイルを見ると、このフックの実装はログに記録された値をモジュールレベルの配列にプッシュするだけで、その配列は React DevTools に表示される情報を収集します。これは本記事の範囲外であり、現時点では文書化が困難なため、詳しくは説明しません。
How useDeferredValue works
This section is not available yet. Please fill an issue.
While waiting for the newsletter, you can get notified when new content drops by following me on X.
How useTransition works
This section is not available yet. Please fill an issue.
While waiting for the newsletter, you can get notified when new content drops by following me on X.
How useSyncExternalStore works
This section is not available yet. Please fill an issue.
While waiting for the newsletter, you can get notified when new content drops by following me on X.
How useId works
This section is not available yet. Please fill an issue.
While waiting for the newsletter, you can get notified when new content drops by following me on X.
付録
これは全てのフックの内部保存値を示す表です:
フック | memoizedState | コメント |
---|---|---|
use | N/A | use はフックのルールに従わず、内部データ構造を使用しません |
useCallback | [callback, deps] | useCallback は渡されたコールバックと依存配列を保存します |
useContext | N/A | useContext はフックの呼び出し順序に依存せず、fiber.dependencies プロパティに保存されます |
useEffect | effect | useEffect はpushEffect によって作成されたエフェクトオブジェクトを保存します。これにはエフェクト関数や依存配列などが含まれます |
useImperativeHandle | effect | useImperativeHandle は内部的にuseLayoutEffect を呼び出します |
useLayoutEffect | effect | useLayoutEffect はpushEffect によって作成されたエフェクトオブジェクトを保存します。これにはエフェクト関数や依存配列などが含まれます |
useInsertionEffect | effect | useInsertionEffect はpushEffect によって作成されたエフェクトオブジェクトを保存します。これにはエフェクト関数や依存配列などが含まれます |
useMemo | [value, deps] | useMemo はキャッシュされた結果値と依存配列を保存します |
useReducer | state | useReducer は memoizedState に状態値のみを保存し、dispatch は queue に保存されます |
useRef | {current: value} | useRef は{current: value} を memoizedState として保存します |
useState | state | useState は特別な reducer を持つuseReducer で、memoizedState に状態値のみを保存し、dispatch は queue に保存されます |
useDebugValue | N/A | useDebugValue は開発者ツールによって注入される空のフックです |
useDeferredValue | TBD | |
useTransition | TBD | |
useSyncExternalStore | TBD | |
useId | TBD |