---
title: "Zustandとは？読み方・使い方とv5の注意点【5.0.16対応】"
url: "https://www.issoh.co.jp/tech/details/7615/"
published: 2025-07-09
updated: 2026-10-31
categories: ["React"]
publisher: "株式会社一創"
---

# Zustandとは？読み方・使い方とv5の注意点【5.0.16対応】

Zustand（ツーシュタント）は、Reactの状態をProviderなしで共有できる軽量な状態管理ライブラリです。2026年10月22日公開の5.0.16が最新で、v5ではセレクタの書き方を誤ると無限ループで画面が止まるなど、v4時代の解説記事のままでは動かない箇所があります。この記事では、インストールからストア作成、useShallow、TypeScript、Sliceパターン、persistによる永続化、Next.js App Routerでの使い方までを、手元で実行した結果（再描画回数・バンドルサイズ・保存内容）と公式ドキュメントの記述をもとに説明します。

## まとめ：Zustand 5.0.16の要点と採用判断

Zustandは `create` 1つで状態と更新関数を定義し、コンポーネントからはセレクタで必要な値だけを読む作りです。最小構成の `create` だけなら、esbuildでminify後のgzipサイズは421バイトでした。

- **版**：最新は5.0.16。React 18以上・TypeScript 4.5以上が必要です。v5.0.xはパッチ更新だけで、5.0.9で実験的な `unstable_ssrSafe` が入りました。
- **セレクタ**：1つの値を返すセレクタはそのまま書き、複数の値をオブジェクトや配列で返すときは `useShallow` で包みます。v5では包まないと「Maximum update depth exceeded」で止まります。
- **persist**：v5はストアを作った時点では保存せず、最初の更新で初めて書き込みます。保存する項目は `partialize` で絞り、トークンなどの秘密情報は入れません。
- **Next.js**：App Routerではモジュールに置いたグローバルストアを使わず、リクエストごとにストアを作ってContextで配ります。Server Componentsからは読み書きしません。
- **選び方**：クライアント側のUI状態を少ないコードで共有したいならZustandが第一候補です。サーバーから取ってくるデータのキャッシュはTanStack Queryに任せ、Zustandに自作しないほうが整理しやすくなります。

## Zustandの意味と読み方・設計の特徴

「Zustand」はドイツ語で「状態」を意味する名詞です。[ドイツ語版Wiktionary](https://de.wiktionary.org/wiki/Zustand)の発音表記は \[ˈt͡suːˌʃtant\] で、日本語では「ツーシュタント」が近い読みです。読み方について、[公式リポジトリのDiscussion](https://github.com/pmndrs/zustand/discussions/1032)でメンテナーのdai-shi氏は「作者はドイツ語風でも英語風でもこだわらない」と答えており、作者のdrcmda氏も「zoostand」などどの読み方でも構わないと書いています。日本では英語風の「ズースタンド」も使われます。開発元はJotaiやValtio、React Three Fiberと同じpmndrs（Poimandres）です。

設計上の特徴は、ストアがReactの外にあるという点です。[公式README](https://github.com/pmndrs/zustand/blob/v5.0.16/README.md)が挙げるとおり、アプリ全体をContext Providerで包む必要がなく、ストアのフックをどのコンポーネントからでも直接呼べます。React Contextは値が変わると、その値を読む全コンポーネントが再描画されます。Zustandはセレクタで選んだ値が変わったコンポーネントだけを再描画するので、更新の多い状態を共有しても影響範囲が広がりません。Contextだけで足りる場面との分け方は[React Contextの使い方｜createContext・useContext・Providerの実装と再レンダー対策【React 19.3対応】](/tech/details/2681/)で解説しています。

Reactバインディングが使うのは `react` パッケージの `useSyncExternalStore`・`useCallback`・`useDebugValue` で（[5.0.16のsrc/react.ts](https://github.com/pmndrs/zustand/blob/v5.0.16/src/react.ts)）、peerDependenciesに `react-dom` を含みません。このためReact Nativeでも同じストア定義が動きます。WebとモバイルでReactのコードを共有する構成は、弊社の[Flutter / React Native開発](/service/apps/cross%5Fplatform/)でも扱っています。

## Zustandのインストールコマンドとバージョン要件（npm・pnpm・yarn）

追加の初期設定はなく、パッケージを入れればすぐ使えます。

```
npm install zustand
# pnpm add zustand
# yarn add zustand

# 入った版の確認
npm ls zustand
```

5.0.16のpackage.jsonが要求する依存は次のとおりです（[npm](https://www.npmjs.com/package/zustand)のメタデータで確認）。peerDependenciesはすべてoptional指定で、使う機能に応じて入れます。

| 依存                      | 要求範囲      | 必要になる場面                  |
| ----------------------- | --------- | ------------------------ |
| react                   | \>=18.0.0 | Reactのフックとして使うとき         |
| @types/react            | \>=18.0.0 | TypeScriptで使うとき          |
| immer                   | \>=9.0.6  | immerミドルウェアを使うとき         |
| use-sync-external-store | \>=1.2.0  | zustand/traditionalを使うとき |

v5はReact 18以上を前提とするため、React 16.8〜17のプロジェクトではv4系を使うか、Reactを18以上に上げます。npmに公開されているv4系の最新は2025年5月15日公開の4.5.7です（npmレジストリの公開履歴で確認）。

## createによるストア作成とコンポーネントでの購読

### createによる状態とアクションの定義

`create` に渡す関数は `set` を受け取り、初期状態と更新関数をまとめたオブジェクトを返します。戻り値はフックなので、慣例として `useXxxStore` と名付け、モジュールで1回だけ定義します。以降のコード例はTypeScriptで書き、TypeScript 7.0.2の `strict` で型エラーが出ないことを確認しています。

```
import { create } from 'zustand'

type CounterState = {
  count: number
  user: { name: string; age: number }
  increment: () => void
  reset: () => void
  setAge: (age: number) => void
}

export const useCounterStore = create<CounterState>()((set) => ({
  count: 0,
  user: { name: 'Taro', age: 20 },
  increment: () => set((state) => ({ count: state.count + 1 })),
  reset: () => set({ count: 0 }),
  // set は1階層目だけを浅くマージする。ネストは自分でコピーする
  setAge: (age) => set((state) => ({ user: { ...state.user, age } })),
}))
```

コンポーネントでは、セレクタ関数を渡して必要な値だけを取り出します。

```
function Counter() {
  const count = useCounterStore((state) => state.count)
  const increment = useCounterStore((state) => state.increment)
  return <button onClick={increment}>{count}</button>
}
```

### setの浅いマージとネストした状態のコピー

`set` は渡したオブジェクトを現在の状態に1階層だけマージします。`user.age` のような2階層目を変えるときに `set({ user: { age } })` と書くと、`user.name` が消えます。上の `setAge` のようにスプレッドで内側もコピーしてください。ネストが深くなる場合は、`immer` を入れて `zustand/middleware/immer` の `immer` ミドルウェアで包むと、`state.user.age = age` のように直接書き換える記法で更新できます。

## セレクタによる再描画の制御（useShallowとv5の無限ループ）

### 実測：セレクタの書き方別の再描画回数

3通りの読み方をするコンポーネントを並べ、ストアの別の値だけを更新したときの描画回数をReact 19.3.1で数えました。jsdom上でStrictModeを使わずに実行し、表の数字はマウントからの累計回数です。

| 読み方                       | 初回 | 無関係な値を更新 | countを更新 |
| ------------------------- | -- | -------- | -------- |
| (s) => s.count            | 1  | 1（描画なし）  | 2        |
| useShallow で count と name | 1  | 1（描画なし）  | 2        |
| 引数なし useCounterStore()    | 1  | 2（再描画）   | 3        |

引数なしで呼ぶとストア全体を購読するので、そのコンポーネントが使っていない値の更新でも再描画されます。画面に出す値ごとにセレクタを書くのが基本です。

### 新しい参照を返すセレクタとv5の無限ループの原因

複数の値をまとめて取り出そうとして、セレクタからオブジェクトを返すと問題が起きます。

```
// v5 では描画が止まる
const { count, name } = useCounterStore((state) => ({
  count: state.count,
  name: state.user.name,
}))
```

セレクタが呼ばれるたびに新しいオブジェクトが作られ、Reactは「値が変わった」と判断して再描画を繰り返します。手元の実行では、まず「The result of getSnapshot should be cached to avoid an infinite loop」の警告が出て、続けて「Maximum update depth exceeded」の例外で描画が止まりました。[v5の移行ガイド](https://github.com/pmndrs/zustand/blob/v5.0.16/docs/reference/migrations/migrating-to-v5.md)も、セレクタが新しい参照を返すと無限ループになりうると明記しています。v4では等価判定の仕組みが違ったため、v4時代の記事のコードをそのまま写すとここで止まります。

直し方は `useShallow` で包むことです。[useShallowの公式リファレンス](https://github.com/pmndrs/zustand/blob/v5.0.16/docs/reference/hooks/use-shallow.md)のとおり、結果を浅く比較し、中身が同じなら前回と同じ参照を返します。

```
import { useShallow } from 'zustand/react/shallow'

export function Profile() {
  // 単一の値はそのまま返す
  const count = useCounterStore((state) => state.count)
  // 複数の値をまとめて返すなら useShallow で包む（v5 では必須）
  const { name, age } = useCounterStore(
    useShallow((state) => ({ name: state.user.name, age: state.user.age })),
  )
  return <p>{name}（{age}）: {count}</p>
}
```

オブジェクトに限らず、`state.items.filter(...)` のように配列を作り直すセレクタや、`state.action ?? (() => {})` のように毎回新しい関数を返すセレクタも同じ原因で止まります。フォールバックの関数はモジュールの定数にしておけば参照が固定されます。

### createWithEqualityFnを使う場面

v5の `create` は独自の等価関数を受け取りません。v4で `create` に `shallow` を渡していたコードを移行するときや、新しいコードでも浅い比較では足りない独自の比較（値の誤差を許すなど）をセレクタごとに使いたいときは、`zustand/traditional` の `createWithEqualityFn` を使います。こちらは `use-sync-external-store` を別途インストールする必要があります。比較が浅い比較で済むなら、追加の依存が要らない `useShallow` で足ります。

## TypeScriptでのストア型定義とcreate<T>()() の理由

TypeScriptでは `create<CounterState>()((set) => ...)` と、型引数のあとに空の `()` をはさみます。[公式の上級TypeScriptガイド](https://github.com/pmndrs/zustand/blob/v5.0.16/docs/learn/guides/advanced-typescript.md)によると、TypeScriptは型引数を「全部指定するか全く指定しないか」しか選べないため、状態の型だけを指定してミドルウェア関係の型引数を推論させる回避策としてカリー化しています。`()` を省くとミドルウェアを重ねたときに型が崩れます。

例外は `combine` や `redux` のように、ミドルウェア自身が状態を作る場合です。この場合は状態の型を推論できるので、同ガイドはカリー化しない書き方を勧めています。

## Sliceパターンによるストアの分割と結合

機能が増えたら、状態とアクションを関心ごとの「スライス」に分け、最後に1つのストアへ結合します。[公式のSliceパターンのガイド](https://github.com/pmndrs/zustand/blob/v5.0.16/docs/learn/guides/slices-pattern.md)は、ミドルウェアは結合したストアにだけ掛け、個々のスライスの中では使わないよう注意しています。スライス内で使うと予期しない動作につながるためです。

```
import { create, type StateCreator } from 'zustand'
import { persist } from 'zustand/middleware'

type BearSlice = { bears: number; addBear: () => void }
type FishSlice = { fishes: number; addFish: () => void }
type Store = BearSlice & FishSlice

const createBearSlice: StateCreator<Store, [['zustand/persist', unknown]], [], BearSlice> = (set) => ({
  bears: 0,
  addBear: () => set((state) => ({ bears: state.bears + 1 })),
})

const createFishSlice: StateCreator<Store, [['zustand/persist', unknown]], [], FishSlice> = (set) => ({
  fishes: 0,
  addFish: () => set((state) => ({ fishes: state.fishes + 1 })),
})

// ミドルウェアは結合したストアにだけ掛ける
export const useBoundStore = create<Store>()(
  persist((...a) => ({ ...createBearSlice(...a), ...createFishSlice(...a) }), { name: 'bound-store' }),
)
```

`StateCreator` の第2型引数には、外側で掛けるミドルウェアを書きます。スライスの中で第3引数のストアAPIからミドルウェアの機能（persistなら `persist.rehydrate` など）を呼ぶときに型を合わせるためのもので、この例のように `set` だけを使うスライスなら、空配列にしても型チェックは通りました。スライスごとにファイルを分けると、機能単位でテストを書きやすくなります。

## 非同期アクションとReact外からの読み書き

非同期処理に専用のミドルウェアは要りません。アクションを `async` 関数にし、待ったあとに `set` を呼びます。ローディングとエラーも同じストアに持たせると、UIの分岐を書きやすくなります。

```
import { create } from 'zustand'

type Todo = { id: number; title: string }
type TodoState = {
  todos: Todo[]
  loading: boolean
  error: string | null
  fetchTodos: () => Promise<void>
}

export const useTodoStore = create<TodoState>()((set) => ({
  todos: [],
  loading: false,
  error: null,
  fetchTodos: async () => {
    set({ loading: true, error: null })
    try {
      const res = await fetch('https://jsonplaceholder.typicode.com/todos?_limit=5')
      if (!res.ok) throw new Error(`HTTP ${res.status}`)
      set({ todos: (await res.json()) as Todo[], loading: false })
    } catch (e) {
      set({ error: (e as Error).message, loading: false })
    }
  },
}))

// React の外（API クライアントやイベントハンドラ）からも読み書きできる
const unsubscribe = useTodoStore.subscribe((state, prev) => {
  if (state.error && state.error !== prev.error) console.warn('取得失敗:', state.error)
})
useTodoStore.getState().fetchTodos().finally(unsubscribe)
```

ストアのフックには `getState`・`setState`・`subscribe` が付いていて、コンポーネントの外からも使えます。`subscribe` のコールバックは新旧の状態を受け取り、戻り値の関数で購読を解除します。`zustand/vanilla` の `createStore` で作ったストアで試すと、2回更新したあとに解除したところ、コールバックの記録は「0->1, 1->10」の2件で、解除後の更新は届きませんでした。

キャッシュ、再取得、ページ単位の読み込みまで必要になったら、それはサーバー状態の管理です。Zustandで自作せず、[TanStack Queryとは？React Queryとの違い・v5の使い方と既定値の落とし穴](/tech/details/10307/)で紹介しているライブラリに任せ、Zustandはクライアント側のUI状態に絞ると役割がはっきりします。

## persistによるlocalStorageへの保存と復元

### v5のpersistの保存タイミング（生成時と初回更新時の実測）

`persist` ミドルウェアで包むと、状態を `localStorage` などに保存し、再読み込み後に復元します。v5（と4.5.5以降）では、ストアを作った時点では何も書き込みません。jsdomのlocalStorageで確かめると、生成直後の `getItem` は `null` で、1回更新したあとに `{"state":{"count":1},"version":0}` が保存されました。v4のように「初期値を必ず保存しておく」前提のコードは、ストア生成の直後に `setState` を1回呼ぶよう[移行ガイド](https://github.com/pmndrs/zustand/blob/v5.0.16/docs/reference/migrations/migrating-to-v5.md)が案内しています。

### partialize・version・migrateの書き方

```
import { create } from 'zustand'
import { persist, createJSONStorage, devtools } from 'zustand/middleware'

type SettingsState = {
  theme: 'light' | 'dark'
  token: string | null
  setTheme: (theme: 'light' | 'dark') => void
}

export const useSettingsStore = create<SettingsState>()(
  devtools(
    persist(
      (set) => ({
        theme: 'light',
        token: null,
        setTheme: (theme) => set({ theme }),
      }),
      {
        name: 'settings',
        storage: createJSONStorage(() => localStorage),
        partialize: (state) => ({ theme: state.theme }), // token は保存しない
        version: 1,
        migrate: (persisted, version) => {
          const s = persisted as { theme?: string; darkMode?: boolean }
          if (version === 0) return { theme: s.darkMode ? 'dark' : 'light' } as SettingsState
          return persisted as SettingsState
        },
      },
    ),
  ),
)
```

- `name` はlocalStorageのキー名です。アプリ内で重複しない名前にします。
- `partialize` で保存する項目を選びます。上の例で `token` を設定しても、保存内容は `{"state":{"theme":"dark"},"version":1}` のままでした。localStorageはページ上の任意のスクリプトから読めるため、認証トークンは保存対象から外します。
- 保存データの形を変えたら `version` を上げ、`migrate` で旧形式を変換します。旧形式 `{"darkMode":true}`（version 0）を置いた状態で読み込むと `theme` は `dark` に変換され、ストレージもversion 1の形で書き直されました。`migrate` を書かずにversionだけ上げると、[persistのリファレンス](https://github.com/pmndrs/zustand/blob/v5.0.16/docs/reference/middlewares/persist.md)のとおり古い保存データは使われません。

保存先をIndexedDBにしたい、容量が気になるといった場合の選び方は[IndexedDBのデメリットと容量上限｜localStorage・Cookieとの違いと使い分け](/tech/details/9666/)にまとめています。

### SSRでの復元タイミングの制御（skipHydration）

サーバーにはlocalStorageがないため、サーバーで描いたHTMLは初期値、ブラウザは保存値で描画し、内容がずれるとハイドレーションエラーになります。`skipHydration: true` を指定すると自動の復元が止まるので、マウント後に `useEffect` の中で `useSettingsStore.persist.rehydrate()` を呼びます。復元が終わったかは `persist.hasHydrated()` で分かりますが、これは購読を伴わない関数で、値が変わってもコンポーネントは再描画されません。表示を切り替えたいときは `onRehydrateStorage` のコールバックか、`persist.onFinishHydration` で自前の状態を更新します。エラー文の読み方は[ハイドレーションエラー（Hydration Error）とは？React 19のエラー文・#418の読み方と原因別の直し方](/tech/details/3218/)で扱っています。

## ミドルウェアの重ね順（devtools・persist・immer）

上のpersistの例で `devtools` を一番外に置いているのには理由があります。[上級TypeScriptガイド](https://github.com/pmndrs/zustand/blob/v5.0.16/docs/learn/guides/advanced-typescript.md)は、`devtools` は `setState` を書き換えて型引数を足すため、ほかのミドルウェアより後（外側）に置くよう勧めています。immerと併用するなら `devtools(immer(...))` で、`immer(devtools(...))` ではありません。`devtools` はブラウザ拡張のRedux DevToolsに状態の変化を送るもので、使い方は[Redux DevToolsの使い方｜React-Reduxの接続とステートの追跡](/tech/details/3060/)を参考にしてください。`devtools` の `enabled` オプションは既定で開発モードでは `true`、本番モードでは `false` です。明示しなければ本番ビルドで状態が拡張機能へ送られることはありませんが、`enabled: true` を書くと本番でも送られるので、条件を付けずに `true` を指定しないでください。

## Next.js App Routerでのリクエスト単位のストア

### 公式が勧めるcreateStore＋Contextの構成

App Routerでは2つの制約があります。サーバーは複数のリクエストを同時に処理するので、モジュールに置いたグローバルストアだと別ユーザーのリクエストと状態が混ざるおそれがあります。またServer Componentsはフックを使えません。[公式のNext.jsガイド](https://github.com/pmndrs/zustand/blob/v5.0.16/docs/learn/guides/nextjs.md)は「グローバルストアを作らない」「Server Componentsからストアを読み書きしない」の2点を推奨し、`zustand/vanilla` の `createStore` で作ったストアをContextで配る構成を示しています。

```
'use client'

import { type ReactNode, createContext, useContext, useState } from 'react'
import { useStore } from 'zustand'
import { createStore } from 'zustand/vanilla'

export type CounterStore = { count: number; increment: () => void }

export const createCounterStore = (initCount = 0) =>
  createStore<CounterStore>()((set) => ({
    count: initCount,
    increment: () => set((state) => ({ count: state.count + 1 })),
  }))

type CounterStoreApi = ReturnType<typeof createCounterStore>
const CounterStoreContext = createContext<CounterStoreApi | undefined>(undefined)

export function CounterStoreProvider({ children, initCount }: { children: ReactNode; initCount?: number }) {
  // リクエスト（ツリー）ごとに1回だけストアを作る
  const [store] = useState(() => createCounterStore(initCount))
  return <CounterStoreContext.Provider value={store}>{children}</CounterStoreContext.Provider>
}

export function useCounterStore<T>(selector: (store: CounterStore) => T): T {
  const store = useContext(CounterStoreContext)
  if (!store) throw new Error('useCounterStore must be used within CounterStoreProvider')
  return useStore(store, selector)
}
```

`useState` の初期化関数で作るので、再描画のたびにストアが作り直されることはありません。Server Componentで取得した値を `initCount` として渡せば、サーバーとブラウザの初期値をそろえられます。ガイドのApp Routerの例はProviderを `layout.tsx` に置いています。ページ（ルート）ごとにストアを作る構成も取れますが、ページ単位でストアを作って配る手間が増えるため、ガイドは必要な場合以外は避けるよう注記しています。App Router自体の動かし方は[Next.jsとは？16系App Routerで最小アプリを動かす手順と採用判断](/tech/details/17373/)で解説しています。

### unstable\_ssrSafe（5.0.9の実験的ミドルウェア）

[5.0.9のリリースノート](https://github.com/pmndrs/zustand/releases/tag/v5.0.9)で、Next.jsでの新しい使い方に向けた実験的ミドルウェア `unstable_ssrSafe` が追加されました。ストア定義を包むと、`window` がない環境をSSRと見なし、その間の `set` と `setState` を例外にします。手元で試すと、SSR扱いのストアで更新した時点で「Cannot set state of Zustand store in SSR」が投げられ、ブラウザ扱いでは通常どおり更新できました。サーバー側での書き込みを禁じれば、Contextなしのグローバルストアでもリクエスト間で状態が混ざらない、という考え方です。名前のとおり不安定版扱いで、Next.jsガイド冒頭にも「議論をもとに近く更新する」との注記があります。本番は上のContext構成で組み、ssrSafeは検証用にとどめるのが安全です。

## Redux Toolkit・Jotai・Contextとの比較と選び方

サイズは、アプリ全体ではなく、表に書いたAPIだけをimportする最小のエントリファイルを測った値です。esbuild 0.28.2で `--bundle --minify --format=esm --platform=browser --external:react --external:react-dom` と `process.env.NODE_ENV` を production に置き換えてバンドルし、出力を `gzip -9c`（標準入力経由のためファイル名はヘッダーに入りません）で圧縮したバイト数です。冒頭の421バイトも同じ手順の値です。週間ダウンロード数は[npm公式のダウンロード数API](https://api.npmjs.org/downloads/point/2026-10-22:2026-10-28/zustand)で2026年10月22日〜28日を指定して2026年10月31日に取得した値を、1万未満で四捨五入したものです（Zustandは85,871,516、Jotaiは9,073,758、@reduxjs/toolkitは45,970,844、Recoilは604,304）。

| ライブラリ（版）                               | 状態の単位         | Provider | gzip後   | 週間DL   |
| -------------------------------------- | ------------- | -------- | ------- | ------ |
| Zustand 5.0.16（createのみ）               | 1つのストア        | 不要       | 421B    | 8,587万 |
| Zustand（＋persist・devtools・useShallow）  | 1つのストア        | 不要       | 3,158B  | 同上     |
| Jotai 3.0.1（atom・useAtom）              | 小さなatom       | 任意       | 3,220B  | 907万   |
| Redux Toolkit 2.13.0＋react-redux 9.3.1 | slice＋reducer | 必要       | 10,575B | 4,597万 |
| Recoil                                 | atom・selector | 必要       | 測定対象外   | 60万    |

Recoilは2025年1月1日にMetaがGitHubリポジトリをアーカイブし、更新が止まっています。新規採用の候補から外してください。

選び方は次のように考えます。

- **Zustandを選ぶ**：ログイン中ユーザー、モーダルの開閉、カートなど、複数の画面で読むクライアント状態を少ない記述で共有したい場合。ストアが1か所にまとまるので、状態の全体像を追いやすくなります。
- **Jotaiを選ぶ**：フォームの各項目のように、細かい状態を部品ごとに持ち、派生値を組み合わせたい場合。違いは[Jotaiとは？atomで組むReact状態管理の使い方とv3の変更点【3.0.1対応】](/tech/details/7660/)で詳しく比べています。
- **Redux Toolkitを選ぶ**：大人数で更新の書き方を統一したい、RTK Queryまで含めて1つの流儀にそろえたい場合。[Redux Toolkitとは？createSliceとRTK Queryの実装と採用判断【2026年8月版】](/tech/details/16013/)が判断材料になります。

Zustandを使わないほうがよいのは、状態がフォーム1画面の中で閉じている場合と、サーバーから取ったデータを持ち回るだけの場合です。前者は `useState` で足り、後者はTanStack Queryのキャッシュをそのまま読めばよく、ストアに写すと二重管理になります。

## v4からv5へ移行するときの変更点

v5.0.0は2024年10月に公開され、新機能より古い仕様の削除が中心です。[移行ガイド](https://github.com/pmndrs/zustand/blob/v5.0.16/docs/reference/migrations/migrating-to-v5.md)が挙げる変更のうち、コードの修正が必要になりやすいものは次のとおりです。

- default exportの廃止：`import create from 'zustand'` は使えず、`import { create } from 'zustand'` に書き換えます。
- React 18・TypeScript 4.5が最低要件になり、`use-sync-external-store` は `zustand/traditional` を使うときだけのpeerDependencyになりました。
- `create` が等価関数を受け取らなくなり、新しい参照を返すセレクタは無限ループの原因になります（上の「セレクタによる再描画の制御」の章）。
- persistが生成時に初期状態を保存しなくなりました（4.5.5も同じ）。
- `setState` の第2引数 `replace` に `true` を渡すときの型が厳しくなりました。UMD・SystemJS・ES5向けのビルドは提供されなくなりました。

5.0.xのパッチは主にpersistとdevtoolsの修正です。5.0.10で同時に呼んだ `rehydrate` の競合、5.0.12で復元後コールバックが古い状態を受け取る不具合、5.0.15で `clearStorage()` と非同期の復元が競合する不具合が直っています（[GitHubのリリース一覧](https://github.com/pmndrs/zustand/releases)）。persistを使っているなら、5.0.0のまま止めずに最新の5.0.16へ上げてください。

## よくある質問

### Zustandの読み方は？

ドイツ語で「状態」を意味する語で、ドイツ語の発音に近いのは「ツーシュタント」です。公式のDiscussionで作者は読み方にこだわらないと述べており、英語風の「ズースタンド」でも構いません。

### Zustandの最新バージョンはいくつ？

2026年10月31日時点の最新は2026年10月22日公開の5.0.16です。手元の版は `npm ls zustand` で確認できます。

### 複数のストアを作ってもよい？

作って構いません。互いに参照しない状態なら用途別に別のストアに分け、互いのアクションから相手の状態を読むならSliceパターンで1つのストアにまとめると型と処理を共有できます。

### useShallowはいつ必要？

セレクタがオブジェクトや配列など新しい参照を返すときです。`(state) => state.count` のように1つの値を返すセレクタには不要です。

### Next.jsのServer Componentsで使える？

使えません。ストアを読むコンポーネントには `'use client'` を付け、Server Componentsで取得した値はProviderの初期値として渡します。

## 関連記事

- [Jotaiとは？atomで組むReact状態管理の使い方とv3の変更点【3.0.1対応】](/tech/details/7660/)
- [Redux Toolkitとは？createSliceとRTK Queryの実装と採用判断【2026年8月版】](/tech/details/16013/)
- [TanStack Queryとは？React Queryとの違い・v5の使い方と既定値の落とし穴](/tech/details/10307/)
- [Nano Storesとは？atom・computed・永続化の使い方とReact/Astro連携【1.5.4対応】](/tech/details/9798/)
- [React Contextの使い方｜createContext・useContext・Providerの実装と再レンダー対策【React 19.3対応】](/tech/details/2681/)

---

出典: [Zustandとは？読み方・使い方とv5の注意点【5.0.16対応】](<https://www.issoh.co.jp/tech/details/7615/>)（株式会社一創）
