Open10

Zustand

まさきちまさきち

Zustandとは何か?

Zustandは、JotaiとReact Springsの開発者によって開発されたFlux/Reduxの流れを汲むステート管理ライブラリ。React向けに必要な機能だけに削ぎ落とし、シンプルなAPIを提供している。1ファイルで完結するシンプルな実装とバンドルサイズの小ささが特徴。




Zustandが生まれた背景

Reduxの課題として、型定義やミドルウェアの設定が複雑であり reducer、action、dispatch など複数のファイルが必要であることが挙げられる。「もっとシンプルに状態管理をしたい」というニーズが強まっていた。

ZustandはReduxのようにスケーラブルで、Contextよりも効率的、かつHooksベースで自然に使える設計となっていることを目的に設計された。
Zustand V3から引き継いだ開発者(加藤 大志氏 Zustand、Jotai、Valtioの作者)によると、その当時「グローバルステート = React Context 内で管理」ことが主流であったが、Context を使わず、React の外で状態を管理するパターンを採用

React Contextの優位性
React Context は値が更新されると、それを参照している全てのコンポーネントが再レンダリングされるが、Zustand のストアは React の外にあるため、ストアが更新されても「必要な state を購読しているコンポーネントだけ」が再レンダリングされる。無駄な際レンダリングを防ぐことでパフォーマンスが向上する

// Zustand (部分的に再レンダー)
const count = useStore((state) => state.count); // countが変わったときだけ再レンダー


Redux や Context ベースの管理では、アプリ全体を <Provider> でラップする必要があるが、Zustand は不要

// Redux
<Provider store={reduxStore}>
  <App />
</Provider>

// Zustand (不要)
<App />
項目 Redux Zustand
グローバル状態 Context(Provider内) React外に作成
再レンダー Provider以下すべて影響 必要なコンポーネントのみ


https://gitnation.com/contents/development-history-of-zustand?utm_source=chatgpt.com

https://levtech.jp/media/article/focus/detail_685/

https://blog.openreplay.com/zustand-state-management-for-react/?utm_source=chatgpt.com

https://www.frontendundefined.com/posts/monthly/react-state-management-libraries-history/?utm_source=chatgpt.com




技術選定

Reduxの「 Fluxパターン 」に基づく 一元的なグローバル状態管理
Jotaiは、Redux, Zustandなどの一元管理と違い、アトムを使った分散型の状態管理
一元管理したいけどシンプルで軽量なライブラリを使いたい

2025年現在、Recoilの公式リポジトリはFacebookによりアーカイブされており、活発な開発は停滞している。

https://qiita.com/masa12Y/items/7737d691b491f34ff269


技術評価

Jotai

「Proxyを使わず、かつ従来のSelectorの仕組みに頼らずにパフォーマンスを最適化する」という長年の課題を、Atomというモデルで解決。Atomにより、状態を小さな単位に分割し、必要な部分だけを購読することで、「自動的に最適化された再レンダリング」を実現する。

RecoilのAPIでAtom等を定義する際に、識別子として文字列のキーが必須であるという点に違和感を覚えました。要するに、JavaScriptオブジェクトの参照そのものを識別子として使えない設計なんです

「文字列キーが不要で、JavaScriptオブジェクトの参照をそのまま識別子として使えて、より小さなAPIで動く、Atomベースの状態管理」という特徴を持つJotaiが誕生する。


https://qiita.com/suin/items/e2df562b0c2be7e2a123

ライブラリのバンドルサイズ比較

再実効性

メモリ使用量


「redux toolkit」または「zustand」]


zustand

https://zustand.docs.pmnd.rs/getting-started/introduction

https://zenn.dev/yoshida_yoshida/articles/e59bc94f7589bd

https://dev.classmethod.jp/articles/zustand_middleware/

https://zenn.dev/y_ta/books/3d5d0951320221/viewer/437bf8

Best practices
You may wonder how to organize your code for better maintenance: Splitting the store into separate slices.
Recommended usage for this unopinionated library: Flux inspired practice.
Calling actions outside a React event handler in pre-React 18.
Testing
For more, have a look in the docs folder

スケールする設計を考えるなら「状態の一元管理 + フォルダ構成による責務分離」を実施する。

/stores
  /user
    index.ts         ← store作成(create)
    types.ts         ← 型定義
    initialState.ts  ← 初期状態
    actions.ts       ← set関数で状態更新



Redux Toolkit

Redux Toolkit:DevTools、middleware、slice 管理

Middleware とは、アクションが reducer に届く前に処理を挟むための機構です。

ログ出力、非同期処理、認証チェックなどを間に挟める
代表的なものに redux-thunk や redux-saga などがある
Redux Toolkit では createAsyncThunk で 非同期処理用のミドルウェアが簡単に使える

Redux Toolkitの場合、Slice 管理を使用して、状態(state)とアクション(reducer)を一つのまとまりとして定義可能

https://zenn.dev/noydmt/articles/3ffa307376a276

まさきちまさきち

Middleware

Redux DevTools、Persistの設定が必要

devtoolsとpersistという2つのミドルウェアを組み合わせる。
devtools(persist(...)) の順番で対応する。

import type { StateCreator } from 'zustand'
import { createJSONStorage, devtools, persist } from 'zustand/middleware'

type MiddlewareOptions = {
  devtoolsName?: string
  persistName?: string
  persistStorage?: Storage
}

/**
 * devtoolsとpersistをラップするミドルウェア
 * @param storeInitializer
 * @param options
 * @returns
 */
export const withMiddleware = <T>(
  storeInitializer: StateCreator<T>,
  options?: MiddlewareOptions
): StateCreator<T, [], [['zustand/devtools', never], ['zustand/persist', T]]> => {
  return devtools(
    persist(storeInitializer, {
      name: options?.persistName ?? 'storage',
      storage: createJSONStorage(() => options?.persistStorage ?? localStorage),
    }),
    {
      name: options?.devtoolsName ?? 'store',
    }
  )
}
```


<br>

### partialize
一部の状態だけを保存

```ts
persist(
  (set, get) => ({
    count: 0,
    user: { name: 'John', loggedIn: false },
    tempValue: 123,
  }),
  {
    name: 'my-storage',
    partialize: (state) => ({
      count: state.count,
      user: state.user,
    }),
  }
)
```

<br>

### Immer
Immer:Immerライブラリを統合して、不変性を簡単に保ちながら状態を更新できるような拡張
Immerは、ミュータブル(直接代入)な書き方で、イミュータブルな状態更新を実現するライブラリ

ネストが深いとスプレッド演算子だらけで読みづらい。
```ts
set((state) => ({
  form: {
    ...state.form,
    contact: {
      ...state.form.contact,
      email: 'test@example.com'
    }
  }
}));
```

Immerを使用すると以下のような書き方ができる。
```ts
set((state) => {
  state.user.name = 'Alice'; // ← ミュータブル風に書いてOK!
});
```

```ts
import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';

interface State {
  user: {
    name: string;
    age: number;
  };
  updateName: (name: string) => void;
}

export const useStore = create<State>()(
  immer((set) => ({
    user: { name: '', age: 0 },
    updateName: (name) =>
      set((state) => {
        state.user.name = name;
      })
  }))
);
```

状態がフラットな場合は不要。
まさきちまさきち

デザインパターン・ベストプラクティス



スライスパターン

Storeを小さく分割する。
機能を追加するにつれて、ストアはどんどん大きくなり、維持することが難しくなる可能性がある場合はメインストアを小さな個別のストアに分割してモジュール化を実現する。

https://zustand.docs.pmnd.rs/guides/slices-pattern#usage-in-a-react-component

export const createFishSlice = (set) => ({
  fishes: 0,
  addFish: () => set((state) => ({ fishes: state.fishes + 1 })),
})
export const createBearSlice = (set) => ({
  bears: 0,
  addBear: () => set((state) => ({ bears: state.bears + 1 })),
  eatFish: () => set((state) => ({ fishes: state.fishes - 1 })),
})
import { create } from 'zustand'
import { createBearSlice } from './bearSlice'
import { createFishSlice } from './fishSlice'

export const useBoundStore = create((...a) => ({
  ...createBearSlice(...a),
  ...createFishSlice(...a),
}))
まさきちまさきち

subscribeWithSelector

「状態の一部(セレクター)にだけ反応して何かしたいとき」に使用する。状態の一部(特定のプロパティ)にだけ反応する購読(subscribe)を可能にする拡張機能。
状態の一部だけを監視可能。対象が変わったときだけ出力できる。

import { create } from 'zustand';
import { subscribeWithSelector } from 'zustand/middleware';

interface StoreState {
  count: number;
  name: string;
  increment: () => void;
}

export const useStore = create<StoreState>()(
  subscribeWithSelector((set) => ({
    count: 0,
    name: 'React',
    increment: () => set((state) => ({ count: state.count + 1 })),
  }))
);


次に、subscribeWithSelector で購読する。

useEffect(() => {
  const unsub = useStore.subscribe(
    (state) => state.count, // セレクタ:countのみに注目
    (newCount, prevCount) => {
      console.log(`countが ${prevCount}${newCount} に変わった`);
    }
  );
  return () => unsub();
}, []);
まさきちまさきち

useShallow

https://zustand.docs.pmnd.rs/guides/prevent-rerenders-with-use-shallow

複数のステートプロパティを shallow equal(浅い比較)で監視する ために使用
ユースケースとしては、状態が頻繁に変わるけどUIには関係ない時に使用できる。
「描画コストが高いコンポーネント」や「頻繁に変化するstate」がある箇所で重点的に使用する。

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

const { count, user } = useStore(
  useShallow((state) => ({
    count: state.count,
    user: state.user,
  }))
);
まさきちまさきち

Auto Generating Selectors

セレクタを自分で書く必要がある。

const count = useStore((s) => s.count);
const name = useStore((s) => s.name);


Auto Generating Selectorsを使用すると、セレクタが自動生成される。

const useStore = createSelectors(
  create((set) => ({
    count: 0,
    name: 'React',
    setCount: (n: number) => set({ count: n }),
  }))
);

// ✅ セレクタが自動生成される!
const count = useStore.use.count(); // ← useStore((s) => s.count) と同等
const name = useStore.use.name();

store.use.key() で済むので記述が楽

キーに対して () => store((s) => s[key]) という関数を生成して、store.use.count() のような 専用セレクター関数を生やす

for (const k of Object.keys(store.getState())) {
  (store.use as any)[k] = () => store((s) => s[k as keyof typeof s]);
}