経験は何よりも饒舌

10年後に真価を発揮するかもしれないブログ 

Reactのコードを読む(3)

前回はhello worldが描写されるところまでをみたので、今回はuseStateをみていこうと思う。

wafuwafu13.hatenadiary.com

まずは、useStateがどこで定義されているのかを確認する。

<!DOCTYPE html>
<html>
<head>
    <script src="https://unpkg.com/react@17/umd/react.development.js"></script>
    <script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
</head>
<body>
</div>
<script>
    console.log(React);
</script>
</body>
</html>

この出力は、以下のようになる。

Children: {map: ƒ, forEach: ƒ, count: ƒ, toArray: ƒ, only: ƒ}
Component: ƒ Component(props, context, updater)
Fragment: Symbol(react.fragment)
...
useEffect: ƒ useEffect(create, deps)
useImperativeHandle: ƒ useImperativeHandle(ref, create, deps)
useLayoutEffect: ƒ useLayoutEffect(create, deps)
useMemo: ƒ useMemo(create, deps)
useReducer: ƒ useReducer(reducer, initialArg, init)
useRef: ƒ useRef(initialValue)
useState: ƒ useState(initialState)
...

つまり、useStateReactのプロパティとして定義されていて、コードでは3552行目でエクスポートされている。
https://github.com/wafuwafu13/react-17-react-dom-17/blob/master/react.development.js#L3552

exports.useState = useState;

次は、console.log(React.useState);によってuseStateを出力してみる。

ƒ useState(initialState) {
    var dispatcher = resolveDispatcher();
    return dispatcher.useState(initialState);
  }

結果をみると、useStateは関数だとわかる。
実際にコードを見にいくと、1531行目に定義されている。
https://github.com/wafuwafu13/react-17-react-dom-17/blob/master/react.development.js#L1531

function useState(initialState) {
      var dispatcher = resolveDispatcher();
      return dispatcher.useState(initialState);
}

では、実際にconsole.log(React.useState());で呼び出してみる。
すると、以下のエラーがでる。

Uncaught 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:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.

このエラーは、1532行目で呼び出されていたresolveDispatcher関数の内部の1501行目で起こっている。
https://github.com/wafuwafu13/react-17-react-dom-17/blob/master/react.development.js#L1501

    function resolveDispatcher() {
      var dispatcher = ReactCurrentDispatcher.current;
  
      if (!(dispatcher !== null)) {
        {
          throw 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:\n1. You might have mismatching versions of React and the renderer (such as React DOM)\n2. You might be breaking the Rules of Hooks\n3. You might have more than one copy of React in the same app\nSee https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem." );
        }
      }
  
      return dispatcher;
}

このエラーは、if (!(dispatcher !== null))trueのとき、つまりdispatchernullのとき生じる。
そのdispatcherに代入されているReactCurrentDispatcherは、115行目で定義されており、確かにReactCurrentDispatcher.currentnullである。
https://github.com/wafuwafu13/react-17-react-dom-17/blob/master/react.development.js#L115

    var ReactCurrentDispatcher = {
      /**
       * @internal
       * @type {ReactComponent}
       */
      current: null
    };

エラー内容に、Hooks can only be called inside of the body of a function component.とあったので、function componentを定義し、その内部でuseStateを呼び出してみる。

そのためにはまず、コンポーネントを作成しないといけない。
コンポーネントの作成には、createElement()を用いる。
公式のReact 要素を作成するに、「JSX のそれぞれの要素は React.createElement() を呼ぶための単なる糖衣構文です。」という説明がなされており、今回はuseStateをみるのが目的なので深追いはしない。
では、以下のようにhelloコンポーネントを作成し、画面にhello worldを描写する。
そのhelloコンポーネント内でuseStateを呼び出して出力してみる。

<body>
    <div id="root"></div>
    <script>
      const hello = () => {
        console.log(React.useState());
        return React.createElement("div", null, "hello world");
      };

      ReactDOM.render(
        React.createElement(hello),
        document.getElementById("root")
      );
    </script>
</body>

出力は以下のように、1つ目の要素がundefined、2つ目の要素が関数の配列が返ってくる。

(2) [undefined, ƒ]
0: undefined
1: ƒ ()
arguments: (...)
caller: (...)
length: 1
name: "bound dispatchAction"
__proto__: ƒ ()
[[TargetFunction]]: ƒ dispatchAction(fiber, queue, action)
[[BoundThis]]: null
[[BoundArgs]]: Array(2)
length: 2
__proto__: Array(0)

先ほどnullになってエラーの原因となっていたdispatcherをみてみると、useStateが含まれていた。

readContext: ƒ (context, observedBits)
unstable_isNewReconciler: false
useCallback: ƒ (callback, deps)
...
useState: ƒ (initialState)
...


dispatcherに代入されているReactCurrentDispatcher.currentがどのように設定されているのかは、以下の記事がわかりやすかった。
renderWithHooks関数は14976行目に定義されている。
https://github.com/wafuwafu13/react-17-react-dom-17/blob/master/react-dom.development.js#L14976

sbfl.net


resolveDispatcherが呼び出されたあとは、dispatcher.useStateが呼び出されている。
このuseStateは16271行目に定義されている。

useState: function (initialState) {
          currentHookNameInDev = 'useState';
          mountHookTypesDev();
          var prevDispatcher = ReactCurrentDispatcher$1.current;
          ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnMountInDEV;
  
          try {
            return mountState(initialState);
          } finally {
            ReactCurrentDispatcher$1.current = prevDispatcher;
          }
},

これはHooksDispatcherOnMountInDEVのプロパティであり、15011行目でReactCurrentDispatcherに代入されている。
https://github.com/wafuwafu13/react-17-react-dom-17/blob/master/react-dom.development.js#L15011

ReactCurrentDispatcher$1.current = HooksDispatcherOnMountInDEV;

useStateの内部ではReactCurrentDispatcherの更新が行われ、15651行目で定義されているmountState関数が呼び出されている。
https://github.com/wafuwafu13/react-17-react-dom-17/blob/master/react-dom.development.js#L15651

    function mountState(initialState) {
      var hook = mountWorkInProgressHook();
  
      if (typeof initialState === 'function') {
        // $FlowFixMe: Flow doesn't like mixed types
        initialState = initialState();
      }
  
      hook.memoizedState = hook.baseState = initialState;
      var queue = hook.queue = {
        pending: null,
        dispatch: null,
        lastRenderedReducer: basicStateReducer,
        lastRenderedState: initialState
      };
      var dispatch = queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber$1, queue);
      return [hook.memoizedState, dispatch];
    }

そこでhook.memoizedStateに引数で受け取ったinitialStateが代入され、返り値の配列の第一引数になっている。
第二引数のdispatchは、16805行目のdispatchActionbindしたものが入っている。

これらの返り値を以下のように分割代入して用いる。

const [foo, setFoo] = useState(bar);

深追いできなかった部分は多かったが、以上でsetStateを読むのは終える。
ReactCurrentDispatcherの更新が沼すぎるのでこれ以上Reactを読むかどうかは分からない、とても同じ人間が書いたとは思えない。
次回からはフロントの繋がりでBabelを読むか、Goのechoとかを読むかもしれない。
DOMの更新に関しては以下の記事がわかりやすかったのでこれを深めると良いと思っている。

kuroeveryday.blogspot.com

これもよさそう。
(翻訳) React Hooks は魔法ではなく、ただの配列だ · GitHub