React系のフレームワークで開発していると、規模が大きくなるにつれ、状態をどう扱えばよいか迷う場面がやってきます。
コンポーネントAのボタンを押すと、遠く離れたコンポーネントBの内容も変わる必要がある状況を想像してください。
このように複数のコンポーネントが共有して使うデータを、グローバル状態と呼びます。
それを効果的に扱うのが状態管理です。
今回は多くの状態管理ライブラリの中でも、簡潔で使いやすいと人気のZustandを紹介します。
状態管理が必要な理由から、順を追って分かりやすく説明します。
Reactでは、親から子へpropsでデータを渡す、上から下へのデータフローを使います。
もし最上位の状態を、ツリーの奥深い子コンポーネントで使いたい場合はどうなるでしょうか。
途中のコンポーネントはその状態を必要としていなくても、すべてを経由してpropsを渡す必要があります。
これが
prop drilling
です。

Prop drilling
コードが複雑になり、保守も難しくなります。
状態管理ライブラリは、この問題を解決するために登場しました。
コンポーネントツリーの外にグローバルストアという保存場所を作り、必要なコンポーネントが直接アクセスして、状態を読んだり更新したりできるようにします。
Zustand以前にも、さまざまな状態管理ツールがありました。
代表的なものを簡単に比較します。
React組み込みの機能で、追加のライブラリなしで使えます。
ただし、Context内の一部だけが変わっても、そのContextを購読するすべてのコンポーネントが不要に再レンダリングされる欠点があります。
テーマや認証情報など、頻繁には変わらない状態に適しています。
歴史が長く、最も有名な状態管理ライブラリです。
Fluxアーキテクチャを基に状態の変更を予測可能にし、ミドルウェアによる非同期処理や強力な開発ツールなど、豊かなエコシステムを持ちます。
一方、一つの状態を追加するだけでもAction、Reducer、Dispatchなどの定型コードが多いという欠点があります。
Facebookが作った状態管理ライブラリで、Reduxの複雑さを改善するために登場しました。
状態をatomという小さな単位に分け、組み合わせてselectorという派生状態を作れます。
Context APIに似た使い方でありながら、状態変更による再レンダリングの最適化も実現しました。
Zustandはドイツ語で「状態」を意味します。
その名前どおり、状態管理に集中した、小さく高速なライブラリです。
ReduxとRecoilの長所を組み合わせ、欠点を改善することを目指して作られました。
とてもシンプル
Reduxのような複雑な設定や定型コードはほとんどありません。数行でストアを作って使えます。
Providerが不要
Context APIと異なり、アプリ全体をProviderで囲む必要がなく、コードをすっきり保てます。
フックを基盤にする
useStateと同じようにフックを呼び、状態と更新関数を取得する直感的な仕組みです。
再レンダリングの最適化
状態の必要な部分だけを購読し、不要な再レンダリングを自動で防ぎます。
百聞は一見にしかずです。
簡単なカウンターを通して、使い方をコードで確認しましょう。
まずプロジェクトへZustandをインストールします。
ストアは、状態とそれを変更する関数をまとめて持つ場所です。
srcの下に
store.tsを作ります。
src/store.tsimport { create } from 'zustand'
const useCounterStore = create((set) => ({
count: 0,
increase: () => set((state) => ({ count: state.count + 1 })),
decrease: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
}))
export default useCounterStore
zustandからcreateをインポートします。
createへ設定関数を渡します。その引数setは状態を更新する関数です。
- ストアに数値の
countと、このcountを変更するincrease、decrease、resetを定義しました。
setを呼ぶ際、現在のstateを受け取り、新しい状態オブジェクトを返して安全に更新します。
作成したストアをReactコンポーネントで使います。
App.tsximport React from 'react'
import useCounterStore from './store'
function Counter() {
const { count, increase, decrease, reset } = useCounterStore()
return (
<div>
<h1>카운터</h1>
<p>{count}</p>
<button onClick={increase}>+1</button>
<button onClick={decrease}>-1</button>
<button onClick={reset}>리셋</button>
</div>
)
}
export default Counter
- 作成した
useCounterStoreをコンポーネントへインポートします。
useCounterStore()を呼ぶと、すべての状態と関数をオブジェクトとして取得できます。
useStateと同じようにcountを表示し、各ボタンのonClickへincreaseとdecreaseを関連付けました。
とても簡単ではないでしょうか。
Providerを設定せず、
createでストアを作り、コンポーネントからフックを呼ぶだけで、グローバル状態を管理できました。
Zustandが便利だからといって、すべてをグローバルストアに入れるのは適切ではありません。
利用範囲に応じて、
グローバル状態
と
ローカル状態
に分けるのが望ましい方法です。
-
- 特定コンポーネント内だけで使い、ほかと共有する必要がない状態です。
- 例:フォームの入力値、モーダルの開閉など。
- React標準の
useStateやuseReducerで管理するのが適しています。
-
- 複数のコンポーネントが共有し、アクセスする状態です。
- 例:ログイン中のユーザー情報、ダークモードなどのテーマ、カートの商品一覧。
- このような場合にZustandなどの状態管理ライブラリを使います。
*「この状態は、ほかのコンポーネントでも必要か」*と考えてみてください。
不要ならローカル状態、必要ならグローバル状態として管理することを検討します。
状態管理の必要性から始めて、既存ツールとZustandを比較しました。
さらに、カウンターの例から基本的な使い方を学びました。
Zustandは複雑な設定なしで強力な状態管理を実現する、直感的かつ効率的なツールです。
今回は最も基本的な部分を紹介しました。
次回は、中心となるselectorとshallow、ストアを操作するさまざまな関数を詳しく見ていきます。