遅延初期化(lazy initialization)とは?
Reactにおける遅延初期化は、状態(state)の初期化時点で初期化作業のコストが高かったり、時間がかかったりする場合に便利に使用できるテクニックです。
遅延初期化が必要な理由
例えば、次のようなコードを見てみましょう。以下のコードでは、Reactコンポーネントが再レンダリングされるたびに、useStateの初期値として渡された関数が毎回呼び出されるという問題があります。
const veryHeavyWork = () => {
console.log('very heavy work called');
// localStorageへのアクセス、複雑な計算、API呼び出しなど
// 時間のかかる作業...
return { data: 'expensive data' };
};
export const ProblemComponent = () => {
// 問題:再レンダリングのたびにveryHeavyWorkが呼び出される
const [expensiveState, setExpensiveState] = useState(veryHeavyWork());
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
増加(再レンダリングが発生)
</button>
</div>
);
};
上記のコードでは、「増加」ボタンをクリックするたびにveryHeavyWork
関数が呼び出されます。しかし、expensiveState
はすでに初期化されてreact-reconcilerによってメモ化されているため、この関数の戻り値は無視され、パフォーマンスの無駄が発生します。
遅延初期化を適用する
このような場合、遅延初期化を使用すると、再レンダリングが行われても初期値として設定されていれば、コストの高い関数を再度実行しないようにできます。方法は簡単です。関数の実行結果を渡すのではなく、関数を返す関数を渡します。
export const OptimizedComponent = () => {
// 解決策:関数を返す関数を渡す
const [expensiveState, setExpensiveState] = useState(() => veryHeavyWork());
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
増加(最適化済み)
</button>
</div>
);
};
つまり、関数の実行タイミングを開発者が直接決めるのではなく、Reactに委任することで解決できます。このように必要になるまで実行を遅らせる技法から「遅延初期化(Lazy Initialization)」という名前が付けられました。
JavaScriptの評価タイミング
初期化
useState
に引数を渡す前に、JavaScriptコードの評価時点でveryHeavyWork
関数がまず実行されます。再レンダリングするということはコンポーネント関数を再度実行することなので、この関数は毎回呼び出されます。関数実行後の戻り値がuseState
に渡されますが、react-reconcilerがフックのmemoizedStateに保存されている既存の値を使用するため、この値は無視されます。
const [expensiveState, setexpensiveState] = useState(veryHeavyWork());
再レンダリング
しかし、以下のように記述すると、veryHeavyWork
関数を実行する前に、関数を返す関数が値としてuseState
に渡されます。したがって、JavaScriptがコードを評価する時点では実行されません。
const [expensiveState, setexpensiveState] = useState(() => veryHeavyWork());
Reactでの判断プロセス
初期化
ではReactではこのような判断をどのように行っているのでしょうか?Reactのコードを分析してみましょう!
初期値が入ってくるとmountState
が実行されます。戻り値としてmemoizedStateとdispatchを返しているのが分かります。これらの値が私たちが使用するconst [state, setState] = useState()
です。
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const hook = mountStateImpl(initialState);
// 省略...
queue.dispatch = dispatch;
return [hook.memoizedState, dispatch];
}
``
次に実装であるmountStateImpl
を見てみましょう。初期値が関数として入ってきた場合、その関数を実行した結果をinitialState
に格納し、その値を再びmemoizedState
に格納しているのが分かります。
function mountStateImpl<S>(initialState: (() => S) | S): Hook {
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
const initialStateInitializer = initialState;
initialState = initialStateInitializer();
// 省略...
}
hook.memoizedState = hook.baseState = initialState;
// 省略..
return hook;
}
再レンダリング
次に再レンダリング時にどのようなコードが動作するかを見てみましょう。再レンダリング時にはrerenderState
→ rerenderReducer
の順に動作します。コードを見れば分かるように、既存に保存されたmemoizedState
を取得して返すだけです。
function rerenderReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
const hook = updateWorkInProgressHook();
let newState = hook.memoizedState;
// 前のレンダリングフェーズでdispatchが呼び出された場合に実行
if (lastRenderPhaseUpdate !== null) {
// 省略
}
return [newState, dispatch];
}
いつ使うと良いか?
上で説明したように、初期値の生成に計算時間がかかる場合に使うと良いです。例えば、localStorage
やsessionStorage
にアクセスする場合、配列に関するメソッドを使用する場合などです。
-
localStorage/sessionStorageへのアクセス
javascriptconst [user, setUser] = useState(() => { const savedUser = localStorage.getItem('user'); return savedUser ? JSON.parse(savedUser) : null; });
-
複雑な初期データの生成
javascriptconst [chartData, setChartData] = useState(() => { return generateComplexChartData(rawData); });
-
大きな配列やオブジェクトの処理
javascriptconst [processedItems, setProcessedItems] = useState(() => { return largeDataSet.map(item => ({ ...item, processed: true, timestamp: Date.now() })); });
useEffectを使用した初期化との違い
useState
の遅延初期化とuseEffect
を使ってコンポーネントがレンダリングされるときに状態を初期化することの間にはいくつかの違いがあります。
初期化タイミング
- 遅延初期化: 状態変数宣言時に直接値を設定します。同期的に動作し、コンポーネントが最初にレンダリングされる前に値が設定されるため、コンポーネントがマウントされて最初のレンダリングが行われる前に値を使用できます。
- useEffect: コンポーネントがレンダリングされた後に初期化されるため、画面上で値が変化するのを感じることができます。
用途
- 遅延初期化: コンポーネントがマウントされる前に値を使用する必要がある場合
- useEffect:非同期作業やサイドエフェクトが発生する場合