Skip to main content

フックの仕組み

はじめに

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関数から呼び出される例外を除く)。

フックは関連するFibermemoizedStateプロパティに保存されます。

フックは React 内部で以下のプロパティを持つプレーンオブジェクトとして保存されます:

const hook: Hook = {
memoizedState: null,

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

next: null,
};

各プロパティの役割:

  • memoizedState: フックの「ステート」(または値)を保持
  • baseState: ステートフックが初期値を保存するために使用
  • baseQueue:
  • queue: ステートフックが様々な情報を保存するための UpdateQueue オブジェクト
  • next: 次のフックを指す

nextプロパティがコンポーネントで使用する次のフックを参照することから、フックは前述のデータ構造のリンクリストとしてモデリングされています。

各フックはこれらのプロパティに何を保存するかについて独自の仕様を持ち、明らかにすべてのプロパティを使用しないフックもあります。

このデータ構造にはフックの種類に関する情報が含まれていないことに注目してください。フックは呼び出し順序に依存し、常に保存される必要があります。

fiber and hook

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関数は前述のデータ構造を作成し、それをcurrentlyRenderingFibermemoizedStateプロパティに設定します。

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;
}
  • 最初にフックオブジェクトを作成します
  • その後、リストの最初のフックである場合、それをcurrentlyRenderingFibermemoizedStateにアタッチし、workInProgressHookにも設定します
  • それ以外の場合、workInProgressHooknextプロパティにアタッチします

以上です!

フックによっては、他の処理も行われますが、それぞれのサポートされるフックについて別々に説明します。

更新時のフックの仕組み

コンポーネントが更新される(初回レンダリングではない)場合、各サポートされるフック呼び出しは以下の式で始まり、その後に特定の処理が続きます。

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 型のオブジェクトを受け入れます。

tip

useフックはmountWorkInProgressHookupdateWIPHookに依存しないため、条件付きで呼び出すことができ、フックの規則に従わなくなります。

実装

前述のように、usethenabledContextの両方を受け入れます:

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 状態のインクリメント(これについては説明しません)の後、useThenabletrackUsedThenableを呼び出し、これがすべての仕事を行います。

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. パート 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. パート 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 が解決または拒否されるまでツリーを一時停止します。

    これにより、コンポーネントはデータがある場合にのみレンダリングされ、それ以外の場合はスローされます。

    note

    useフックは、エラーをキャッチするためにツリーにエラーバウンダリを配置する必要があります。

danger

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]
}

直接的には存在しない関数ですが、前述のようにmountCallbackupdateCallback関数があり、同じシグネチャを持ちます:

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 オブジェクトを指します。

実装

useContextreadContextForConsumer関数を使用します:

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のレンダリングの仕組みで説明されます。

note

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 はすべてのレンダーフェーズの終了時に実行されます。
note

レンダーフェーズ中に状態更新が発生しても、effect は 2 回実行されません。effect は commit フェーズで実行されます。

マウント時の実装

通常のフックと同様、mountWorkInProgressHook()を使用します。mountEffectmountEffectImpl関数を呼び出します。

note

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 自体を定義するフラグ。可能な値はInsertionLayoutPassiveuseEffectでは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 を前のlastEffectnextとして追加します(これが新しい最後の要素になります)
    • 新しい effect(新しい最後の要素)はnextプロパティでfirstEffectを指すようになります
    • 最後に、新しい effect をコンポーネントのupdateQueueリストの lastEffect としてマークします

最終的に、pushEffect関数は新しく作成された effect オブジェクトを返し、hook.memoizedStateに保存します。

Implementation on update

更新時の実装

更新時、useEffectHooksDispatcherOnUpdateディスパッチャーから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
);
}
tip

これが 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関数です

tip

コミットフェーズ中、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 のクリーンアップ関数を返却

これで完了です!

note

前述の通り、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と同じです。唯一の違いは、マウント時と更新時にmountEffectImplupdateEffectImplに渡される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 はブラウザのペイント前に実行される」と言われる理由です。

useLayoutEffectClassComponentのライフサイクルメソッド(componentDidMountcomponentDidUpdate)と同じタイミングで実行されます。

シグネチャ

他の effects と同様、useInsertionEffectは以下のように定義されます:

function useLayoutEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null
): void {
// [Not Native Code]
}

実装

useLayoutEffectの実装はuseEffectと同じです。唯一の違いは、マウント時と更新時にmountEffectImplupdateEffectImplに渡される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: 依存関係

マウント時の実装

マウント時、useMemomountMemoを呼び出します。実装は以下の通り:

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: キャッシュされた値を返却
note

開発モードで StrictMode が有効な場合、React はnextCreateを 2 回呼び出します:

// renderWithHooks関数で初期化
if (shouldDoubleInvokeUserFnsInHooksDEV) {
nextCreate();
}

アップデート時の実装

アップデート時、useMemoupdateMemoを呼び出します。実装は以下の通り:

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関数が呼び出されます:

  1. work in progress hook をマウント:

    予想通り、最初に行うのはこれです。理由がわからない場合は、このセクション全体をスキップした可能性があります 😑

    const hook = mountWorkInProgressHook();
  2. 初期 state を計算: 初期 state の計算は、第三引数を指定したかどうかに依存します:

    let initialState;
    if (init !== undefined) {
    initialState = init(initialArg);
    } else {
    initialState = initialArg as S;
    }

    initialArgそのものか、それに関数が適用された結果のいずれかになります。

  3. hook のmemoizedStateプロパティを割り当て:

    hook.memoizedState = hook.baseState = initialState;
  4. 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;
  5. 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を持つ可能性があり、キューは処理待ちのアップデートを保持)にアップデートをスケジュールすることです。

言い換えれば、これはuseReduceruseStateが提供するdispatch関数の実体です。

これで、関数コンポーネントにおける state 更新関数の仕組みを理解できます。

この部分を理解するには、how root.render() worksセクションを事前に読むと理解が容易です。重複を避けるため、スケジューリングの基本的な仕組みは同じであることを前提に説明します。

  1. アップデート lane を要求

    const lane = requestUpdateLane(fiber);

    最初のステップは、アップデートに使用するlaneを決定します。例:

    • アプリケーションがコンカレントモード(createRoot().render())でレンダリングされていない場合、SyncLaneが使用される
    • レンダーフェーズ中のアップデートの場合、最高優先度の lane が使用される(または指定された Lanes 番号内で最小の lane)
    • startTransitionでラップされた場合、トランジション lane の 1 つ
    • root レンダースケジュールの仕組みで説明したイベント優先度
  2. アップデートオブジェクトを作成

    const update: Update<S, A> = {
    lane,
    revertLane: NoLane,
    action,
    hasEagerState: false,
    eagerState: null,
    next: null,
    };

    アップデートオブジェクトは複数のプロパティを持ちますが、ここで重要なのはlaneactionです。React は後で処理できるようこれらの情報を保持します。nextプロパティはリンクリストを形成するために使用されます。

  3. レンダーフェーズ中に呼び出された場合、レンダーフェーズアップデートをキューに追加

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

    レンダーフェーズアップデートは、fibercurrentlyRenderingFiberまたはその 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(最後のアップデート)としてマーク
  4. レンダーフェーズ外で呼び出された場合、コンカレントフックアップデートをエンキューし、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 を処理するためのマイクロタスクがスケジュールされます。

tip

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が受け取った初期値initialValuecurrentプロパティを持つ JavaScript オブジェクトを生成
  • ステップ 3: このオブジェクトをフックのmemoizedStateに保存
  • ステップ 4: ref オブジェクトを返却

アップデート時の実装

アップデート時、useRefupdateRef関数を呼び出します。この実装は非常にシンプルで追加の説明は不要です:

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 のmemoizedStatebaseStateに初期値を設定
  • ステップ 4: update queue オブジェクトを作成し hook の queue に割り当て
tip

これで完了です!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コメント
useN/Auseはフックのルールに従わず、内部データ構造を使用しません
useCallback[callback, deps]useCallbackは渡されたコールバックと依存配列を保存します
useContextN/AuseContextはフックの呼び出し順序に依存せず、fiber.dependenciesプロパティに保存されます
useEffecteffectuseEffectpushEffectによって作成されたエフェクトオブジェクトを保存します。これにはエフェクト関数や依存配列などが含まれます
useImperativeHandleeffectuseImperativeHandleは内部的にuseLayoutEffectを呼び出します
useLayoutEffecteffectuseLayoutEffectpushEffectによって作成されたエフェクトオブジェクトを保存します。これにはエフェクト関数や依存配列などが含まれます
useInsertionEffecteffectuseInsertionEffectpushEffectによって作成されたエフェクトオブジェクトを保存します。これにはエフェクト関数や依存配列などが含まれます
useMemo[value, deps]useMemoはキャッシュされた結果値と依存配列を保存します
useReducerstateuseReducerは memoizedState に状態値のみを保存し、dispatch は queue に保存されます
useRef{current: value}useRef{current: value}を memoizedState として保存します
useStatestateuseStateは特別な reducer を持つuseReducerで、memoizedState に状態値のみを保存し、dispatch は queue に保存されます
useDebugValueN/AuseDebugValueは開発者ツールによって注入される空のフックです
useDeferredValueTBD
useTransitionTBD
useSyncExternalStoreTBD
useIdTBD