Reactのコードを読む(3)
前回はhello world
が描写されるところまでをみたので、今回はuseStateをみていこうと思う。
まずは、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) ...
つまり、useState
はReact
のプロパティとして定義されていて、コードでは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
のとき、つまりdispatcher
がnull
のとき生じる。
そのdispatcher
に代入されているReactCurrentDispatcher
は、115行目で定義されており、確かにReactCurrentDispatcher.current
はnull
である。
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
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行目のdispatchAction
をbindしたものが入っている。
これらの返り値を以下のように分割代入して用いる。
const [foo, setFoo] = useState(bar);
深追いできなかった部分は多かったが、以上でsetState
を読むのは終える。
ReactCurrentDispatcher
の更新が沼すぎるのでこれ以上Reactを読むかどうかは分からない、とても同じ人間が書いたとは思えない。
次回からはフロントの繋がりでBabelを読むか、Goのechoとかを読むかもしれない。
DOMの更新に関しては以下の記事がわかりやすかったのでこれを深めると良いと思っている。