何のために状態管理ライブラリを使用するのか

状態管理ライブラリをどのように使用すべきかはこれまで議論され尽くしていると思うが、現時点での自分の考えを書いてみる。

TL;DR

  • レンダリングのパフォーマンス
  • ステートの適切な責務配置

状態管理ライブラリとは?

主にUIライブラリの外側で管理される状態の管理機構を指す。
Reduxに始まり、 Recoil, jotai, Zustand などがある。

これらは厳密には「グローバルスコープな」状態管理ライブラリである。
多くのUIライブラリはコンポーネント毎に状態を保持することができ、これもコンポーネント単位での「状態管理」である。

const UserCard = () => {
  // useState で状態管理
  const [name, setName] = useState("")

  return <input value={name} onChange={e => setName(e.target.value)} />
}

レンダリングパフォーマンス問題

まず、コンポーネントの状態管理機能のみでアプリケーション全体の状態管理を行なった場合のことを考える。

UIライブラリでは 親コンポーネント -> 子コンポーネント へ状態の受け渡しができるので、 融通する必要のあるデータを全てルートコンポーネントに持ってしまいさえすれば、アプリケーション全体の状態を管理することができる。

コンポーネントは、ステートに変更が加えられた際に子コンポーネントの再描画を行う。
問題は、配下にある多くのコンポーネントにpropsを受け渡す必要が出てくる場合、再描画されてほしくない(する必要のない)コンポーネントにも全て再描画処理が走ってしまうことにある。

例えば以下のようなコンポーネントツリーがあり、コンポーネントAでの入力値変更がコンポーネントEに反映されて欲しいケースがあったとする。コンポーネントAとEの状態を同期させるには、ルートコンポーネントに状態をもたせ、propsとしてコンポーネントに状態を伝播させる。

コンポーネントツリー

このAのコンポーネントでのステートの更新をトリガーとしてpropsを伝播させる際に、配下の全てのコンポーネントに描画処理が走る。

ルートにステートを持つことによって、全てのコンポーネントツリーが更新される

そうすると更新時のコンポーネント描画のコストが、コンポーネントが多くなるにつれ大きくなっていく。

このパフォーマンス問題を解消するのが状態管理ライブラリで、状態をライブラリ側(UIライブラリの外側)で管理することで、状態が変更した際の描画を、その管理している状態に依存しているUIコンポーネントのみにとどめることができる。

状態の責務

親コンポーネントに状態を持ちすぎるとパフォーマンスの問題のみでなく、

  • 親コンポーネントに保持する状態が膨大になる
  • 子コンポーネントに直接関係のないpropsがリレーされる

など、コードの見通しなども悪くなる。
状態管理ライブラリで管理している値は、必要なコンポーネントから直接参照されるだけなので、そのような問題も解消される。

とはいえ、必須ではない

先述しているが、状態管理ライブラリを導入しないとアプリケーションが完成しないということではなく、パフォーマンス問題や状態の責務の問題が致命的にならない状況であれば、基本的に必要のないものである。

また状態の責務に関して言えば、例えばReactだとContext APIを使用すれば、propsのバケツリレーを抑えることができる。(が、パフォーマンス問題は解消されない。)

結論

状態管理ライブラリを使用する目的は描画パフォーマンスの最適化であり、そしてアプリケーションのコンポーネントツリーが一定の規模になる場合にのみ、必要になるのではないか、という結論。