404 Not Found

404 Not Found


nginx

状態管理:Zustand / Redux

トムのショッピングカート機能は当初、useStatepropsを使用してデータを層ごとに渡すことで実装されていましたが、かろうじて機能する程度でした。しかし、コンポーネントツリーが深くなるにつれて(App → Header → CartIcon、App → ProductList → ProductCard → AddToCartButton)、ショッピングカートの状態を変更するには、5層ものコンポーネントを介してコールバック関数を渡す必要があり、ほんのわずかなミスでもコールバックが見落とされたり、誤って渡されたりしてしまう可能性がありました。彼は次のように気づきました:どのコンポーネントでも共有状態を直接読み書きできる、グローバルな状態管理ソリューションが必要だ


1. 学習内容


2. 概念図

100%
flowchart LR
    subgraph Zustand[Zustand Pattern]
        A1[create API] --> B1[Store<br/>state + actions]
        B1 --> C1[Component A<br/>Subscribe Now]
        B1 --> C2[Component B<br/>Subscribe Now]
        B1 --> C3[Component C<br/>Subscribe Now]
    end

    subgraph Redux[Redux Toolkit Pattern]
        A2[createSlice] --> B2[Slice<br/>reducers + actions]
        B2 --> C4[configureStore]
        C4 --> D[Provider<br/>Wrapper Root Component]
        D --> E1[useSelector<br/>Read Status]
        D --> E2[useDispatch<br/>Trigger an update]
    end

    style A1 fill:#e1f5fe,stroke:#0288d1
    style B1 fill:#fff3e0,stroke:#f57c00
    style C4 fill:#e8f5e9,stroke:#388e3c
    style D fill:#c8e6c9,stroke:#2e7d32

左:Zustand—プロバイダーなし、直接サブスクリプション、そして最小限のAPI。右:Redux Toolkit—完全なワークフローを提供し、大規模なチームでの共同作業に適している。


3. 実際の事例

トムのショッピングカートには、いくつかの機能が必要です。具体的には、商品一覧ページにある「カートに入れる」ボタン、ヘッダーにあるショッピングカートのアイコン(商品数を表示するもの)、ショッピングカートページ(商品一覧と合計金額を表示するもの)、そして注文後にショッピングカートを空にする機能です。これらのコンポーネントはコンポーネントツリーの異なるレベルに分散しており、useState + props を使用して渡すのは手間がかかるだけでなく、エラーも発生しやすくなります。

(1) なぜContextを代用できないのですか?

ReactのContextは、確かにプロップが複数のレベルにわたって伝播されるのを防ぎますが、これには2つの重大な問題があります:

専用の状態管理ライブラリ(Zustand、Redux Toolkit)は、選択的サブスクリプションの仕組みを通じてパフォーマンスの問題に対処しています。コンポーネントは、自身に関係する状態の断片のみを監視し、関係のない更新によって再レンダリングがトリガーされることはありません。

ソリューション パフォーマンス プロバイダー 選択的サブスクリプション 習得の難易度 パッケージサイズ
useState + props ✅ ローカルでは高速 なし 該当なし 極めて低い 0
コンテキスト + useReducer ❌ 完全な再レンダリング 必須 0
ステータス ✅ オプションのサブスクリプション 不要 ✅ ネイティブ 約1KB
Redux Toolkit ✅ useSelector 必須 in 約12KB

(2) 状態:ミニマリスト的アプローチ

Zustandの根底にある考え方は、1つのフックですべてを処理するというものです:create() これにより、状態とアクションを同時に定義でき、返されたフックはProviderでラップする必要なく、どのコンポーネントでも直接使用できます。

BASH
npm install zustand

(1) ▶ サンプル:Zustand ショッピングカートストア

JSX
import { create } from 'zustand'

// Definition store Types of(TypeScript Project Recommendations)
/*
type CartItem = { id: number; name: string; price: number; qty: number }
type CartStore = {
  items: CartItem[]
  total: number
  addItem: (product: Omit<CartItem, 'qty'>) => void
  removeItem: (id: number) => void
  updateQty: (id: number, qty: number) => void
  clearCart: () => void
}
*/

// Create store — state and  actions They're all defined here
const useCartStore = create((set, get) => ({
  // --- state ---
  items: [],
  total: 0,

  // --- actions ---
  addItem: (product) => set((state) => {
    const existing = state.items.find(i => i.id === product.id)
    const newItems = existing
      ? state.items.map(i =>
          i.id === product.id ? { ...i, qty: i.qty + 1 } : i
        )
      : [...state.items, { ...product, qty: 1 }]
    return {
      items: newItems,
      total: newItems.reduce((sum, i) => sum + i.price * i.qty, 0)
    }
  }),

  removeItem: (id) => set((state) => {
    const newItems = state.items.filter(i => i.id !== id)
    return {
      items: newItems,
      total: newItems.reduce((sum, i) => sum + i.price * i.qty, 0)
    }
  }),

  updateQty: (id, qty) => set((state) => {
    const newItems = qty <= 0
      ? state.items.filter(i => i.id !== id)
      : state.items.map(i => i.id === id ? { ...i, qty } : i)
    return {
      items: newItems,
      total: newItems.reduce((sum, i) => sum + i.price * i.qty, 0)
    }
  }),

  clearCart: () => set({ items: [], total: 0 })
}))

// Use in any component
function CartIcon() {
  // Subscribe to items.length only,The page will only be re-rendered when the shopping cart list changes.
  const itemCount = useCartStore(state => state.items.length)

  return <span className="cart-badge">Shopping Cart ({itemCount})</span>
}

function ProductCard({ product }) {
  const addItem = useCartStore(state => state.addItem)

  return (
    <div>
      <h3>{product.name}</h3>
      <p>${product.price}</p>
      <button onClick={() => addItem(product)}>Add to Cart</button>
    </div>
  )
}

function CartPage() {
  const { items, total, updateQty, removeItem, clearCart } = useCartStore()

  if (items.length === 0) return <p>Your shopping cart is empty</p>

  return (
    <div>
      <h2>Shopping Cart</h2>
      {items.map(item => (
        <div key={item.id}>
          <span>{item.name}</span>
          <input
            type="number"
            value={item.qty}
            min="0"
            onChange={e => updateQty(item.id, Number(e.target.value))}
          />
          <span>${item.price * item.qty}</span>
          <button onClick={() => removeItem(item.id)}>Delete</button>
        </div>
      ))}
      <hr />
      <p><strong>Total:${total}</strong></p>
      <button onClick={clearCart}>Empty Cart</button>
    </div>
  )
}
▶ 試してみよう

Zustandの主な特徴:

(2) ▶ サンプル:Zustand パーシステンス・ミドルウェア

トムは、ユーザーがショッピングカートのデータを失うことなくページを更新できるようにしたいと考えています。Zustandのpersistミドルウェアは、ストアのデータをlocalStorageに自動的に同期させることができます。

JSX
import { create } from 'zustand'
import { persist } from 'zustand/middleware'

// persist Middleware Package store Definition
const useCartStore = create(
  persist(
    (set, get) => ({
      items: [],
      total: 0,
      addItem: (product) => set((state) => {
        const existing = state.items.find(i => i.id === product.id)
        const newItems = existing
          ? state.items.map(i =>
              i.id === product.id ? { ...i, qty: i.qty + 1 } : i
            )
          : [...state.items, { ...product, qty: 1 }]
        return {
          items: newItems,
          total: newItems.reduce((sum, i) => sum + i.price * i.qty, 0)
        }
      }),
      removeItem: (id) => set((state) => {
        const newItems = state.items.filter(i => i.id !== id)
        return {
          items: newItems,
          total: newItems.reduce((sum, i) => sum + i.price * i.qty, 0)
        }
      }),
      clearCart: () => set({ items: [], total: 0 })
    }),
    {
      name: 'cart-storage',  // localStorage 's  key
      // Store all fields by default,That's fine, too. partialize Select which fields to store
      // partialize: (state) => ({ items: state.items }),
    }
  )
)

// The way it works remains exactly the same
function CartIcon() {
  const itemCount = useCartStore(state => state.items.length)
  return <span>Shopping Cart ({itemCount})</span>
}

// localStorage Data Format Stored in the System:
// key: 'cart-storage'
// value: '{"state":{"items":[...],"total":99},"version":0}'
▶ 試してみよう

Persistenceミドルウェアの仕組み: 状態の変更はすべて自動的にlocalStorageに同期され、アプリケーションの起動時にはlocalStorageからデータが自動的に復元されます。ユーザーがページを更新しても、ショッピングカートの状態は完全に保持されます。

(3) ▶ サンプル:Zustand DevTools でのデバッグ

開発段階において、トムはRedux DevToolsと同様に、状態の変化履歴を確認したいと考えていました。Zustandのdevtoolsミドルウェアは、ブラウザのDevTools拡張機能とシームレスに連携します。

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

// Using Multiple Middleware Components Simultaneously:Combine using a nested approach
const useStore = create(
  devtools(
    persist(
      (set) => ({
        count: 0,
        increment: () => set(state => ({ count: state.count + 1 })),
        decrement: () => set(state => ({ count: state.count - 1 })),
      }),
      { name: 'app-storage' }
    ),
    { name: 'AppStore' }  // DevTools As shown in store Name
  )
)

// Open in a browser Redux DevTools Expand
// As you can see "AppStore" All items under this tag state Change Log
// Supports time-travel debugging(Jump to any historical state)
▶ 試してみよう

Zustand ミドルウェアシステムの特徴: ミドルウェアは、ネストされた関数の組み合わせを通じて、外層から内層へと順次実行されます。devtools(persist(...)) は、まず DevTools が接続され、その後に永続化処理が行われることを示しています。複数のミドルウェアコンポーネントを柔軟に組み合わせることで、さまざまなニーズに対応できます。例えば、immer ミドルウェアを追加して変数構文を有効にしたり、各アクションをログに記録するカスタムミドルウェアを作成したりすることができます。


4. Redux Toolkit:エンタープライズグレードのソリューション

プロジェクトが拡大し、複数のチームが連携するようになると、Zustandの柔軟性がかえって問題となる場合があります。開発者によってアクションの記述スタイルが統一されなくなる可能性があるからです。Redux Toolkitは、チーム全体で一貫したパターンを徹底させる標準化された状態管理ワークフローを提供します。

BASH
npm install @reduxjs/toolkit react-redux

(1) ▶ サンプル:Redux Toolkit のワークフロー全体

JSX
import { createSlice, configureStore } from '@reduxjs/toolkit'
import { Provider, useSelector, useDispatch } from 'react-redux'

// ========== 1. Create Slice(Definition state + reducers)==========
const counterSlice = createSlice({
  name: 'counter',
  initialState: { value: 0, history: [] },
  reducers: {
    increment: (state) => {
      state.value += 1
      state.history.push(`+1 → ${state.value}`)
    },
    decrement: (state) => {
      state.value -= 1
      state.history.push(`-1 → ${state.value}`)
    },
    incrementByAmount: (state, action) => {
      state.value += action.payload
      state.history.push(`+${action.payload} → ${state.value}`)
    },
    reset: (state) => {
      state.value = 0
      state.history = []
    }
  }
})

// Automatically generated action creators
export const { increment, decrement, incrementByAmount, reset } = counterSlice.actions

// ========== 2. Layout Store ==========
const store = configureStore({
  reducer: {
    counter: counterSlice.reducer,
    // More can be added later. slice:
    // cart: cartSlice.reducer,
    // user: userSlice.reducer,
  }
})

// ========== 3. Provider Inject ==========
function App() {
  return (
    <Provider store={store}>
      <Counter />
    </Provider>
  )
}

// ========== 4. Used in components ==========
function Counter() {
  // Selective Reading of Characters
  const value = useSelector(state => state.counter.value)
  const history = useSelector(state => state.counter.history)
  const dispatch = useDispatch()

  return (
    <div>
      <h2>Counter:{value}</h2>
      <div>
        <button onClick={() => dispatch(increment())}>+1</button>
        <button onClick={() => dispatch(decrement())}>-1</button>
        <button onClick={() => dispatch(incrementByAmount(5))}>+5</button>
        <button onClick={() => dispatch(reset())}>Reset</button>
      </div>
      <div>
        <h3>Transaction History</h3>
        <ul>
          {history.slice(-5).map((h, i) => (
            <li key={i}>{h}</li>
          ))}
        </ul>
      </div>
    </div>
  )
}
▶ 試してみよう

Redux Toolkitの主要な概念:

概念 機能 類推
createSlice 関連するステートとリデューサーのセットを作成する ファイル引き出し:整理と保管
configureStore すべてのスライスを統合してグローバルストアを作成する ファイルキャビネット:一元管理
Provider Reactコンポーネントツリーへのストアの注入 電源コード:差し込む
useSelector ストアから状態フラグメントを読み込む 引き出しを開いてファイルを取り出す
useDispatch ステータスを更新するアクションをトリガーする 管理者にアクションを実行するよう指示する

(2) ▶ サンプル:Redux Toolkit における非同期処理

実際のプロジェクトにおける状態の更新には、多くの場合、非同期ロジック(ログインやユーザー情報の取得など)が関わってきます。Redux Toolkit には、非同期操作のライフサイクル段階(pending、fulfilled、rejected)に対するサポートが組み込まれています。

JSX
import { createSlice, configureStore, createAsyncThunk } from '@reduxjs/toolkit'
import { Provider, useSelector, useDispatch } from 'react-redux'

// 1. Defining Asynchronous thunk(Automatically Generated pending/fulfilled/rejected Three types action)
const fetchUser = createAsyncThunk(
  'user/fetchUser',      // action Type prefix
  async (userId, { rejectWithValue }) => {
    try {
      const response = await fetch(`/api/users/${userId}`)
      if (!response.ok) throw new Error('Failed to retrieve the user')
      return await response.json()
    } catch (err) {
      return rejectWithValue(err.message)
    }
  }
)

// 2. Create slice
const userSlice = createSlice({
  name: 'user',
  initialState: {
    data: null,
    loading: false,
    error: null
  },
  reducers: {
    clearUser: (state) => {
      state.data = null
      state.error = null
    }
  },
  // Handling Asynchronous Operations thunk The Three States of
  extraReducers: (builder) => {
    builder
      .addCase(fetchUser.pending, (state) => {
        state.loading = true
        state.error = null
      })
      .addCase(fetchUser.fulfilled, (state, action) => {
        state.loading = false
        state.data = action.payload
      })
      .addCase(fetchUser.rejected, (state, action) => {
        state.loading = false
        state.error = action.payload
      })
  }
})

export const { clearUser } = userSlice.actions

// 3. Layout store
const store = configureStore({
  reducer: { user: userSlice.reducer }
})

// 4. Used in components
function UserProfile({ userId }) {
  const dispatch = useDispatch()
  const { data, loading, error } = useSelector(state => state.user)

  return (
    <div>
      <button onClick={() => dispatch(fetchUser(userId))}>Load Users</button>
      {loading && <p>Loading......</p>}
      {error && <p>Error:{error}</p>}
      {data && <p>{data.name} — {data.email}</p>}
    </div>
  )
}
▶ 試してみよう

createAsyncThunk は、以下の 3 つの状態を自動的に処理します: リクエストが開始されると、pending をディスパッチします → 成功時には、fulfilled(データを含む)をディスパッチします → 失敗時には、rejected(エラーメッセージを含む)をディスパッチします。開発者は、extraReducers 内で各条件に応じて状態がどのように変化するかを定義するだけで済みます。useEffectfetch を直接使用する場合と比較して、このパターンの利点としては、リクエストステータスの自動追跡、DevTools によるアクションのログ記録、および複数のコンポーネントが同じサンクの結果にサブスクライブできることが挙げられます。


5. モデルの比較

ディメンション 状態 Redux Toolkit
コード量 ごくわずか—create()関数が1つだけ 適度—slice、store、Providerの全機能
習得の難易度 低(10分で始められる) 中(リデューサー、アクション、ディスパッチ、サンクについての理解が必要)
プロバイダー 不要 プロバイダーパッケージである必要がある
選択的サブスクリプション ネイティブサポート、セレクタ関数 useSelector + 浅い比較
TypeScript ネイティブ対応 良好(型推論あり)
ミドルウェア persist / devtools / immer など 組み込みのサンク + 拡張可能なサガ/エピック
非同期処理 アクション内で手動で実装 createAsyncThunk に組み込み済み
パッケージサイズ 約1KB 約12KB
ユースケース 中小規模のアプリケーション、コンポーネント間の状態共有 大規模なアプリケーション、複雑なデータフロー、チーム間の連携
DevTools 対応済み(ミドルウェアが必要) ネイティブ対応、タイムトラベルデバッグ

選び方のポイント:


❓ よくある質問

Q ZustandとContext + useReducerのどちらを選べばよいですか?
A Zustandの方がパフォーマンスに優れています。Contextが更新されるとすべてのコンシューマーが再レンダリングされますが、Zustandはきめ細かな選択的なサブスクリプションに対応しています。また、Zustandは必要なコード量が少なく、Providerのネストを回避できます。Context + useReducerは、非常に単純なシナリオ(テーマの切り替えなど)には適していますが、ほとんどの場合、Zustandの方が良い選択です。
Q 従来のReduxと比べて、Redux Toolkitはどのような点を簡素化していますか?
A 従来のReduxでは、アクション型の定数、アクションクリエーター、リデューサーのswitch-case文を手動で記述する必要があるほか、Redux DevToolsやミドルウェアも個別に設定する必要があります。Redux ToolkitのcreateSliceはアクションタイプとアクションクリエーターを自動生成し、configureStoreはミドルウェアとDevToolsを自動的に統合し、createAsyncThunkは非同期ワークフローを簡素化します。これにより、コード量が約60%削減されます。
Q Zustand ストアはコンポーネントの外でも使用できますか?
A はい。useCartStore.getState() どこからでも現在の状態を取得でき、useCartStore.setState(...) どこからでも状態を更新できます。これは、ルートガード、インターセプター、WebSocket コールバックなどで状態にアクセスする際に非常に便利です。
Q 同じプロジェクト内で、この2つのライブラリを同時に使用することはできますか?
A もちろんです。多くのプロジェクトでは、最初はZustandから始め、特定のモジュールの複雑さが増してきた段階でRedux Toolkitを導入しています。この2つは競合しません。Zustandは単純なグローバル状態(テーマ、ユーザー情報など)を管理し、Redux Toolkitは複雑なビジネスモジュール(注文処理、承認ワークフローなど)を管理します。
Q useReducerではニーズを満たせない場合にのみ、状態管理ライブラリを使用すべきですか?
A useReducerは、単一のコンポーネント内や局所的なスコープにおける複雑な状態ロジックに適しています。ZustandやRedux Toolkitを導入すべきなのは、互いに関連のない複数のコンポーネント間で状態を共有する必要がある場合、状態を永続化する必要がある場合、あるいはミドルウェアが必要な場合に限られます。単に「使うためだけに」導入してはいけません。
Q Zustandのset関数とReactのsetStateの違いは何ですか?
A Zustandのsetはデフォルトで浅いマージを実行しますが(クラスコンポーネントのsetStateと同様)、ImmerをベースにしたRedux Toolkitのリデューサーは、深い不変の更新を実行します。また、Zustandではimmerミドルウェアを使用することで、Redux Toolkitと同様の可変更新構文を実現することも可能です。どちらのアプローチにも使用上の長所と短所がありますので、チームの好みに応じて選択してください。

📖 まとめ


📝 練習問題

  1. Zustand を使用して「Todo Store」を作成する。タスクの追加、完了ステータスの変更、タスクの削除、完了済みタスクの一括削除、および未完了タスクの件数の集計に対応させること。リストコンポーネントと統計コンポーネントの両方でこれを使用し、選択的サブスクリプションの有効性を検証する。
  2. Todo Store に persist ミドルウェアを追加し、ページを更新した後もデータが保持されるかどうかを確認します。
  3. Redux Toolkit を使用してユーザー管理モジュールを実装する:createAsyncThunk を使用して、ユーザー一覧の取得をシミュレートし(データが返されるまで 1 秒の遅延を設ける)、3 つの状態(読み込み中、エラー、データ)を管理し、コンポーネント内で読み込み状態、エラー状態、およびデータ一覧を表示する。
Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%