やあ。今日も元気にコンポーネントの再レンダリングと戦っているかい?
フロントエンドをやっていると、一度はこんな悩みにぶつかるはずだ。「おいおい、このコンポーネントのツリー、どこまで深くPropsをバケツリレーしなきゃいけないんだ……?」あるいは、「Context APIを使ったら、中身がちょっと変わっただけで配下の全コンポーネントが盛大に再レンダリングされて画面がカクつくんだけど!」ってね。
そう、Reactの標準機能だけでグローバルな状態管理をやろうとすると、ボアアップしすぎた原チャリみたいな無理が生じる。そこで登場するのが、今回の主役「Zustand(ズースタンド)」だ。
今回は、実務で毎日泥臭くReactを書いている君たちに向けて、Zustandの基本から、ブラウザの裏側の動き、そして現場で即採用したくなるスマートなストア設計まで、シニアの視点からみっちり解説していこう。
—
なぜ今、Zustandなのか?
Reduxの全盛期を知る古参なら、あの長大なボイラープレート(アクション定義、リデューサー、ミドルウェアの設定……)を思い出して胃が痛くなるかもしれない。RecoilやJotaiも素晴らしいが、Atomic系は時として「状態の粒度をどう切るか」で設計が複雑化しがちだ。
その点、Zustandはいい意味で「頭がバカになるくらいシンプル」だ。
名前の通りドイツ語で「状態」を意味するこいつは、フックベースの極小ステートマネージャー。Reduxのコンセプトを極限まで削ぎ落とし、Reactの`useState`の延長線上で使える手軽さを持ちながら、裏側では驚くほど洗練されたパフォーマンス最適化を行っている。
裏側で何が起きているのか?(ブラウザとReactの視点)
ちょっと立ち止まって、ブラウザとReactの裏側の話をしよう。
Reactの標準的な`useState`や`useContext`は、「状態が変化したら、それを購読(subscribe)しているコンポーネントツリーを再レンダリングする」という仕組みをとっている。そのため、Contextの値をオブジェクトで渡したりすると、関係ないプロパティが変更されただけで、そのContextを購読している全コンポーネントが容赦なく再描画(Re-render)の刑に処される。
一方、Zustandの裏側の仕組みは実にエレガントだ。
ZustandはReactの外側(Vanilla JSの空間)にメモリストアを持つ。コンポーネントがZustandのフックを使うとき、実は内部で`useSyncExternalStore`(React 18の肝だね)を巧みに利用している。これにより、「自分が本当に必要としているプロパティの変更」だけをピンポイントで監視し、それ以外の変更では再レンダリングを完全にスルーする。
つまり、無駄な再レンダリングが走らない。これが、実務の現場でZustandがパフォーマンスの救世主として崇められている理由さ。
—
実践:Zustandで堅牢なストアを作ろう
百聞は一見にしかず。さっそく、実務でよくある「ユーザー認証情報と、アプリのテーマ(ダークモード等)を管理するストア」を書いてみよう。
まずはインストールからだ。
npm install zustand
そして、これが現場のクオリティで作るストアのコードだ。そのままエディタに貼り付けて動かせるようにしてある。
// src/store/useAppStore.ts
import { create } from ‘zustand’;
import { persist, createJSONStorage } from ‘zustand/middleware’;
// 1. 管理する状態とアクションの型定義(TypeScriptの恩恵を最大限に受ける)
interface User {
id: string;
name: string;
email: string;
}
interface AppState {
user: User | null;
theme: ‘light’ | ‘dark’;
isLoading: boolean;
// アクションの定義
login: (userData: User) => void;
logout: () => void;
toggleTheme: () => void;
}
// 2. create関数を使ってストアを定義
// persist ミドルウェアを使うことで、リロードしてもlocalStorageに状態を保持できる実務必須のテクニック
export const useAppStore = create
persist(
(set, get) => ({
// 初期状態
user: null,
theme: ‘light’,
isLoading: false,
// ログイン処理(状態の更新)
// Reduxと違って、直接オブジェクトをsetするだけでImmerのようなイミュータブルな更新が実現できる
login: (userData) => {
set({ user: userData, isLoading: false });
},
// ログアウト処理
logout: () => {
set({ user: null });
},
// テーマの切り替え
// 現在の状態(get())を参照してトグルする
toggleTheme: () => {
const currentTheme = get().theme;
set({ theme: currentTheme === ‘light’ ? ‘dark’ : ‘light’ });
},
}),
{
name: ‘app-storage’, // localStorageのキー名
storage: createJSONStorage(() => localStorage), // ストレージの指定
// 秘匿情報や一時的なローディング状態などは永続化から除外したい場合の対策(partialize)
partialize: (state) => ({ user: state.user, theme: state.theme }),
}
)
);
コードの解説とシニアからのアドバイス
1. `set` と `get` の使い分け:
状態を更新したいときは`set({ key: value })`を呼ぶだけだ。わざわざ面倒なディスパッチ関数やアクションクリエイターを書く必要はない。複雑な条件分岐で現在の状態を見たいときは、引数の`get()`を叩けばいつでも現在のステートが取れる。
2. ミドルウェア(`persist`)の実用性:
現場では「画面をリロードしたらログイン情報が消えた!」なんてバグ(仕様と言い張る開発者もいるが)はユーザー体験を最悪にする。Zustandは標準で`persist`ミドルウェアを用意してくれているので、一行追加するだけで`localStorage`や`sessionStorage`への永続化が完了する。
—
コンポーネントでのフックの利用と「再レンダリングの罠」
さて、作成したストアを実際のコンポーネントでどう使うか見ていこう。ここがZustandの真骨頂であり、一番注意すべきポイントだ。
// src/components/UserProfile.tsx
import React from ‘react’;
import { useAppStore } from ‘../store/useAppStore’;
export const UserProfile: React.FC = () => {
// 【アンチパターン例】
// const { user, logout } = useAppStore();
// ↑これだと、themeが切り替わっただけでこのコンポーネントも再レンダリングされてしまう!
// 【ベストプラクティス】
// 必要なプロパティだけをセレクター(Selector)を使ってピンポイントで取得する
const user = useAppStore((state) => state.user);
const logout = useAppStore((state) => state.logout);
if (!user) {
return
ログインしていません。
;
}
return (
ようこそ、{user.name} さん
{user.email}
);
};
セレクターパターンをマスターせよ
中級者へのステップアップとして絶対に覚えておいてほしいのが、セレクターを使った細粒度な購読(Granular Subscription)だ。
Zustandのフックに何もセレクターを渡さないと、ストア全体のオブジェクトが返され、ストア内の何らかの値が1つでも変わるたびにそのコンポーネントは再レンダリングの対象になる。
必ず `useAppStore((state) => state.xxx)` のように、必要なプロパティや、複数のプロパティをまとめたカスタムセレクターを渡す習慣をつけよう。
もし、複数の値を同時に取得したい場合は、Zustandの`useShallow`ユーティリティを使うのが現代のフロントエンド開発のスタンダードだ。
import { useShallow } from ‘zustand/react/shallow’;
import { useAppStore } from ‘../store/useAppStore’;
// 複数取得する場合のスマートな書き方
const { theme, toggleTheme } = useAppStore(
useShallow((state) => ({
theme: state.theme,
toggleTheme: state.toggleTheme,
}))
);
これをしておけば、オブジェクトの参照の不一致による不要な再レンダリングを綺麗に防ぐことができる。
—
まとめ:明日からの開発に向けて
Zustandの魅力、そして実務でどう扱うべきか、その空気感が伝わっただろうか。
- 小さく始めて、必要に応じて分割する(一つの巨大なストアにすべてを詰め込まず、`useUserStore`、`useCartStore`のようにドメインごとにファイルを分けるのがコツだ)。
- セレクターを使って無駄な再レンダリングを防ぐ(パフォーマンスのチューニングは細部が命を救う)。
- ミドルウェアを賢く使ってボイラープレートを削減する。
ReactのState管理で消耗していた日々は今日で終わりだ。Zustandを武器に、もっとシンプルで、もっと保守性の高い、美しいコードベースをチームに持ち帰ってくれ。
それじゃあ、次のコードレビューで会おう。健闘を祈る!

コメント