はじめに
こんにちは!株式会社80&Companyの技術広報です。
弊社の開発部署では毎週火曜日の朝9:30から社内勉強会を行なっています。
今回の記事では、フロントエンドエンジニアが社内勉強会で「useStateが動く仕組み」というテーマで発表したものを紹介します。
Reactの特徴とuseStateの内部実装をエントリーポイントから徐々に読み解く過程について説明したものです。
useStateが動く仕組みに興味のある方は、ぜひ参考にしてみて下さい♪
読者の対象
- Reactを使用して開発を行っている方
- useStateの仕組みについて知りたい方
- useStateの内部実装を読み解いたことがない方
Reactの特徴
今回はuseState
の仕組みについて説明しますが、useState
はフロントエンド開発で使用されるJavaScriptライブラリ「React」で利用頻度の高いReact Hookの1つです。
そのため、最初はReactについて簡単に説明します。
Reactは近年人気度が上昇しているJavaScriptライブラリです。
下記のGoogleトレンドのグラフを見ても分かる通りVue、Angularと比べてもかなり人気度が高いライブラリであると言えます。
下記でReactの大きな特徴を2点説明します。
宣言的なview
Reactの特徴の1つ目として、宣言的なviewがあります。
宣言的なviewとは、状態に応じた処理やUIの表示内容を宣言できるviewのことです。
従来では、命令的という概念でUIが実装されていました。
命令的である場合、処理ごとに細かい記述が必要ですが、直感的ではなく不便でした。
宣言的なviewでは、予め1回命令していた処理をまとめて宣言することで、直感的なUIの構築が可能です。
// input要素の取得
const input = document.querySelector(".input");
// input要素でイベントが発火したら、何かしらの関数を実行
input.addEventListener("input", inputFunc);
// 入力した値を反映させるのに必要な要素の取得
const p = document.getElementsByTagName("p");
function inputFunc(event) {
const message = event.target.value;
// 入力反映
const p1 = p[0];
p.innerText = "「" + message + "」という値が入力されました。";
// 文字数カウント
const p2 = p[1];
p2.innerText = message.length;
}
const App = () => {
const [message, setMessage] = useState('')
const handleOnChange = (e: React.ChangeEvent<HTMLInputElement>) => {
e.preventDefault()
setMessage(e.target.value)
}
return (
<div>
<input type='text' onChange={handleOnChange} />
<p>「{message}」という値が入力されました。</p>
<p>{message.length}</p>
</div>
)
}
コンポーネントベース
Reactの特徴の2つ目として、コンポーネントベースがあります。
コンポーネントベースとは、コンポーネントという単位を組み合わせてUIを開発することです。
コンポーネントは値を『状態(state)』として保持することができます。
UIの機能や役割などに応じて、DOMを分割することで、複雑なUIを構築しやすく安全な実装をすることができます。
useState
上記でReactの概要についてお話したため、次は今回のメインテーマとなるuseState
について説明します。
useState
はReact hooksの一つで、関数コンポーネントで状態を管理するための機能です。
useState
を呼び出した際に現在の状態と、状態更新用の関数を返します。
また、状態がない場合はuseState
に渡した値が初期値として使われます。
const [value, setValue] = useState(0)
今回はReactのコードでuseState
の使い方を見ていきます。
useStateのエントリーポイント
まずはuseState
のエントリーポイントから見ていきましょう。
エントリーポイントとは、プログラムやアプリケーションが実行される際の最初の処理を指す言葉です。
この処理が行われると、プログラムやアプリケーションが実行され、動作を開始します。
このエントリーポイントであるuseState
は、stateの初期値(initialState)を受け取って、stateの値とその値を更新するための関数を返します。
Dispatchを使用することで、stateの値を変更することができます。
export function useState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
useState
内では、resolveDispatcher
という関数が呼び出されます。
resolveDispatcher
は、現在useState
を実行しているコンポーネントから、状態を管理するためのdispatcherオブジェクトを取得します。
そしてdispatcherオブジェクトのuseState
が呼び出され、returnによって、状態用の値と更新用の関数が返されます。
resolveDispatcher()
上記のuseState内で、resolveDispatcher
という関数が呼び出されています。
resolveDispatcher
はreact関数内の現在のDispatcherインスタンスを取得するために使用されています。Dispatcherは、Reactアプリケーションの状態を管理するために一般的に使用されるfluxライブラリが提供する型です。
ReactCurrentDispatcher.current
プロパティを利用して、現在のDispatcherインスタンスを取得し、その型を型アサーションを使用して確認します。
この処理はランタイムエラーを回避するために行われているらしいです。
function resolveDispatcher() {
const dispatcher = ReactCurrentDispatcher.current;
return ((dispatcher: any): Dispatcher);
}
ReactCurrentDispatcherの中身
ReactCurrentDispatcher
は、オブジェクトでcurrentというプロパティがあります。
このオブジェクトは、Reactアプリケーション内の現在のDispatcherインスタンスへのアクセスを提供するオブジェクトです。
currentプロパティは最初にnullが設定されますが、アプリケーションが初期化されると通常、Dispatcherインスタンスが設定されます。
これにより、Reactアプリケーションの他の部分がDispatcherインスタンスにアクセスし、使用したアプリケーションの状態を確認することができます。
最初にnullを設定する理由は、ランタイムエラーが発生しないようにするためです。
const ReactCurrentDispatcher = {
current: (null: null | Dispatcher),
};
renderWithHooksの一部
renderWithHooks
関数について説明しますが、なぜReactCurrentDispatcher
の次にこの関数を見ていくのかというと、renderWithHooks
はHooksが使用するコンポーネントがレンダリングされる際に呼び出されるからです。
実際にコードを見ていきましょう。
まずif文の中の処理(_ DEV _)というのは、デバック処理であるかいないかを見ているため、今回はelse文のみに注目していきたいと思います。
if (__DEV__) {
……
} else {
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
}
else文では、ReactCurrentDispatcher.current
に条件式で判定を行い、その結果を格納しています。
条件式はcurrentがnull、またはcurrent.memoizedStateがnullの場合、HooksDispatcherOnMount
が設定されます。
逆にそうではない場合、HooksDispatcherOnUpdateが設定されます。
currentというのは現在のReactコンポーネントの場合を表すものです。
currentに関わる条件で初回レンダリングかどうかを判定している箇所です。
HooksDispatcherOnMount
次に、初回レンダリングの処理であるHooksDispatcherOnMount
の処理を見ていきましょう。
HooksDispatcherOnMount
は、Hooksのデータを管理するためのオブジェクトを定義しています。
KeyにuseState
などの各Hooksが設定され、値にマウント時に呼ぶべき関数が定義されています。
useState
が使用された場合は、mountState
が呼び出されます。
const HooksDispatcherOnMount: Dispatcher = {
//(省略)
useRef: mountRef,
useState: mountState,
useDebugValue: mountDebugValue,
//(省略)
};
mountState
上記のmountState
は、initialStateを引数として受け取っています。
もしこのinitialStateが関数で渡された場合は、関数を実行して初期状態を取得しています。
この初期状態を元に、状態を保持するためのオブジェクトを作成し、queueという変数に格納します。
そして状態を変更するためのDispatch関数を作成し、returnとして初期値と更新するための関数であるDispatchを返しています。
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
// $FlowFixMe: Flow doesn't like mixed types
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
const queue: UpdateQueue<S, BasicStateAction<S>> = {
pending: null,
interleaved: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any),
};
hook.queue = queue;
const dispatch: Dispatch<
BasicStateAction<S>,
> = (queue.dispatch = (dispatchSetState.bind(
null,
currentlyRenderingFiber,
queue,
): any));
return [hook.memoizedState, dispatch];
}
dispatchSetState
先ほど関数として作成したDispatchは、dispatchSetState
をラップしたものです。
この関数は更新するためのアクションを受け取り、作成されたUpdateオブジェクトを設定します。
isRenderPhaseUpdate関数を呼び出し、レンダリング中かどうかを判定しています。
レンダリング中の場合は、enqueueRenderPhaseUpdate関数を呼び出して更新をキューに追加します。
またレンダリング中ではない場合は、enqueueConcurrentHookUpdate関数を呼び出して更新をキューに追加します。
function dispatchSetState<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
) {
//(省略)
const lane = requestUpdateLane(fiber);
const update: Update<S, A> = {
lane,
action,
hasEagerState: false,
eagerState: null,
next: (null: any),
};
if (isRenderPhaseUpdate(fiber)) {
enqueueRenderPhaseUpdate(queue, update);
} else {
enqueueConcurrentHookUpdate(fiber, queue, update, lane);
//(省略)
}
ここまでで、useStateの仕組みについて深掘りするためのコードリーディングは終了です。
感想
発表者がuseStateの内部実装を読み進めた際の感想を下記に2点提示します。
- Reactのソースコードを初めて読み進めた時は、React Fiberアーキテクチャやリコンシリエーションなどの概念を理解する必要があり、レベルが高くて理解が難しかった
- Reactでは、更新優先順位を制御するためにキューというデータ構造を使用しているなど、コンピュータサイエンスの分野が役に立つところを見ることができた
最後に
今回は、弊社社内勉強会で発表された「useStateが動く仕組み」について扱いました。
今後も継続的に80&Companyでの社内勉強会の取り組みを発信していきます!
Qiita OrganizationやTwitter公式アカウントのフォローもよろしくお願いいたします!
最後まで読んでいただきありがとうございます!
参考文献