useCallbackは関数を作成しないらしいので実装を見て他に知らない仕様がないか確認する

## はじめに
`useCallback`はReactのライフサイクル間でキャッシュされた関数を返すReactのhooksです。

主に依存配列等で比較するReactの機能で、関数が同一であることを保証するために利用されます。
```tsx:親の変更を子に伝えない
const Parent = () => {
  const something = useCallback(() => {
    ...
  }, []);
  
  return (
    <div>
      {/* ... */}
      <Child something={something} />
    </div>
  );
};

const Child = memo((something) => {
  // ...
}, []);
```
上記の例で`something`を`useCallback`で囲っていなければ、`Parent`のレンダリングのたびに新しい`something`関数が作成されるため、`Child`のレンダリングも`Parent`に合わせて実行されます。
`somthing`を`useCallback`で囲うことで、`Parent`のレンダリング間で同一の`something`関数が利用されるため、`Child`のレンダリングは実行されません。

```tsx:useEffectの頻繁な発火を防ぐ
const Example = () => {
  const something = useCallback(() => {
    // ...
  }, []);

  useEffect(() => {
    const timeoutId = setTimeout(() => {
      something();
    }, 1000);

    return () => {
      clearTimeout(timeoutId);
    };
  }, [something]);

  ...
};
```
上記の例は初回のレンダリング後に`something`を使った`useEffect`の第一引数の処理を実行します。
`something`が`useCallback`に囲まれていなければ`Example`のレンダリングのたびに`something`が作り直されるので、`useEffect`は依存配列が変化したとみなして、レンダリングのたびに第一引数の処理が実行されます。

さて、私は`useCallback`について1つ勘違いをしていました。異なるライフサイクル間で同じ関数を返してくれるので、関数は毎回作られないと思っていたのです。

しかし、[Reactのドキュメント](https://ja.react.dev/reference/react/useCallback#should-you-add-usecallback-everywhere)には

> useCallback は関数の作成を防ぐわけではないことに注意してください。あなたは常に関数を作成しています（それは問題ありません！）。しかし、何も変わらない場合、React はそれを無視し、キャッシュされた関数を返します。

`useCallback`はただキャッシュを返すだけで、常に関数を作成すると書かれています。
それもそのはずで、`useCallback`の第一引数に対象の関数をそのまま渡しているので、`useCallback`を呼び出すタイミングで関数は作られているのです。

この件は解決しましたが、この他にも`useCallback`に知らない動作が存在するか気になりました。
この記事ではその胸のつっかえをとるべく、`useCallback`の実装について追っていきます。

## useCallbackの実装を探す
`useCallback`は[`react/packages/react/src/ReactHooks.js`](https://github.com/facebook/react/blob/518d06d26a97df6d4f5b04e529e5018ad35ea936/packages/react/src/ReactHooks.js#L113)から世界が広がります。
```js
export function useCallback<T>(
  callback: T,
  deps: Array<mixed> | void | null,
): T {
  const dispatcher = resolveDispatcher();
  return dispatcher.useCallback(callback, deps);
}
```
`resolveDispatcher`を呼び出し、そこに登録された`useCallback`を呼び出しているようです。

[同じファイルのL26](https://github.com/facebook/react/blob/518d06d26a97df6d4f5b04e529e5018ad35ea936/packages/react/src/ReactHooks.js#L26)にその実装があります。
`__DEV__`の部分は無視すると、`ReactSharedInternals`の`H`が返されていました。
```js
function resolveDispatcher() {
  const dispatcher = ReactSharedInternals.H;
  return ((dispatcher: any): Dispatcher);
}
```

`ReactSharedInternals.H`を定義する部分を探すと、[`react-reconciler`の`renderWithHooks`](https://github.com/facebook/react/blob/518d06d26a97df6d4f5b04e529e5018ad35ea936/packages/react-reconciler/src/ReactFiberHooks.js#L534)で実装されていました。

対象の箇所だけ掘り出すと以下のようになっています。
```js
ReactSharedInternals.H =
      current === null || current.memoizedState === null
        ? HooksDispatcherOnMount
        : HooksDispatcherOnUpdate;
```
初期に実行する`HooksDispatcherOnMount`と、それ以降に実行する`HooksDispatcherOnUpdate`で分かれているようですね。

[HooksDispatcherOnMount](https://github.com/facebook/react/blob/518d06d26a97df6d4f5b04e529e5018ad35ea936/packages/react-reconciler/src/ReactFiberHooks.js#L3945)はここに記述されており、`useCallback`は`mountCallback`が呼ばれているようでした。
```js
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,
  useHostTransitionStatus: useHostTransitionStatus,
  useFormState: mountActionState,
  useActionState: mountActionState,
  useOptimistic: mountOptimistic,
  useMemoCache,
  useCacheRefresh: mountRefresh,
};
```
[HooksDispatcherOnUpdate](https://github.com/facebook/react/blob/518d06d26a97df6d4f5b04e529e5018ad35ea936/packages/react-reconciler/src/ReactFiberHooks.js#L3978)もすぐ下にあります。
```js
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,
  useHostTransitionStatus: useHostTransitionStatus,
  useFormState: updateActionState,
  useActionState: updateActionState,
  useOptimistic: updateOptimistic,
  useMemoCache,
  useCacheRefresh: updateRefresh,
};
```

同じファイル内に[`mountCallback`](https://github.com/facebook/react/blob/518d06d26a97df6d4f5b04e529e5018ad35ea936/packages/react-reconciler/src/ReactFiberHooks.js#L3032)と[`updateCallback`](https://github.com/facebook/react/blob/518d06d26a97df6d4f5b04e529e5018ad35ea936/packages/react-reconciler/src/ReactFiberHooks.js#L3039)が書かれています。

```js
function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  hook.memoizedState = [callback, nextDeps];
  return callback;
}

function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;
  if (nextDeps !== null) {
    const prevDeps: Array<mixed> | null = prevState[1];
    if (areHookInputsEqual(nextDeps, prevDeps)) {
      return prevState[0];
    }
  }
  hook.memoizedState = [callback, nextDeps];
  return callback;
}
```
ここが、`useCallback`の本実装なようですね。

## useCallbackの実装を読む

まず、目につくのが[`mountWorkInProgressHook`](https://github.com/facebook/react/blob/518d06d26a97df6d4f5b04e529e5018ad35ea936/packages/react-reconciler/src/ReactFiberHooks.js#L1011)と[`updateWorkInProgressHook`](https://github.com/facebook/react/blob/518d06d26a97df6d4f5b04e529e5018ad35ea936/packages/react-reconciler/src/ReactFiberHooks.js#L1032)です。話が逸れすぎるのでそれぞれの詳細までは追わないことにします。
`mountWorkInProgressHook`はhooksが呼び出された最初のタイミングに使われます。hooksの情報を格納する場所を作っており、その場所はhooksが呼ばれた順番に作られます。
`updateWorkInProgressHook`は`mountWorkInProgressHook`で作成された場所からhooksの順番に従って情報を取り出します。(コンポーネントのトップレベルで呼び出さなければいけないのはこれが理由です)

なんとなくの役割が分かったところで、`mountCallback`の実装を確認します。
```js
function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  hook.memoizedState = [callback, nextDeps];
  return callback;
}
```
`mountWorkInProgressHook`で情報を配置できる箇所を確保して、そこにコールバック関数と依存配列を保存して、コールバック関数を返しています。
`mountCallback`では情報を保存して、元の関数を返すだけのようですね。

次に、`updateCallback`です。
```js
function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;
  if (nextDeps !== null) {
    const prevDeps: Array<mixed> | null = prevState[1];
    if (areHookInputsEqual(nextDeps, prevDeps)) {
      return prevState[0];
    }
  }
  hook.memoizedState = [callback, nextDeps];
  return callback;
}
```
`updateWorkInProgressHook`で前回の情報を取り出しています。
そして、依存配列がない場合を除いて、`areHookInputsEqual`で前回の依存配列の状態と今回の依存配列の状態で比較しています。
[areHookInputsEqual](https://github.com/facebook/react/blob/518d06d26a97df6d4f5b04e529e5018ad35ea936/packages/react-reconciler/src/ReactFiberHooks.js#L485)は依存配列でループを回して状態の比較をする関数です(状態を比較する方法は`is`で実装されています)。
```js
function areHookInputsEqual(
  nextDeps: Array<mixed>,
  prevDeps: Array<mixed> | null,
): boolean {
  for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
    // $FlowFixMe[incompatible-use] found when upgrading Flow
    if (is(nextDeps[i], prevDeps[i])) {
      continue;
    }
    return false;
  }
  return true;
}
// react/packages/shared/objectIs.js
function is(x: any, y: any) {
  return (
    (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y)
  );
}
```
依存配列の状態が同じと判断された場合は前回から登録されていた関数、つまりキャッシュされた関数を返します。
そして、依存配列の状態に変化があれば`updateWorkInProgressHook`で取り出した情報を更新して、最新の関数を返します。

以上が`useCallback`の実装でした。特に想像と違うとこはありませんでした。

[Reactのドキュメントの`useMemo`と`useCallback`の関係](https://ja.react.dev/reference/react/useCallback#how-is-usecallback-related-to-usememo)によると

>すでに useMemo に詳しい場合、useCallback を次のように考えると役立つかもしれません。
```js
// Simplified implementation (inside React)
function useCallback(fn, dependencies) {
  return useMemo(() => fn, dependencies);
}
```

と書かれていたので、`useCallback`は`useMemo`の実装に依存しているかもと考えていましたが、依存していないことを確認できたのでよかったです。

## おわりに
`useCallback`で常にキャッシュされる仕様を知らなかったので、他にも知らない仕様があるか確認するために`useCallback`の実装を確認しました。
目新しいものはなかったですが、具体的な実装方法を知れてよりReactの深淵に近づけた気がしました。
皆さんも気になるhooksがあれば確認してみてはいかがでしょうか。