ReactでZustandを使った状態遷移管理の実装例を徹底解説

Zustandは、軽量でシンプルな状態管理ライブラリとしてReactエコシステムで注目されています。本記事では、Zustandを使用して状態遷移を効率的に管理する方法を解説します。従来の状態管理ツールと比較して直感的で柔軟性が高く、小規模なプロジェクトから大規模なアプリケーションまで対応可能なこのライブラリは、特にステップごとに状態を切り替える機能を簡潔に実現できます。初心者から上級者までを対象に、実践的なコード例を交えながら、Zustandの基本的な使い方から応用例までを詳しく紹介します。

目次

Zustandとは何か


Zustandは、React向けに設計された軽量な状態管理ライブラリです。ReduxやContext APIといった従来のツールと比較すると、シンプルで直感的なAPIを提供しており、学習コストが低いのが特徴です。

Zustandの主な特徴

  • 軽量性: コアはわずか数KBのサイズで、アプリケーションのパフォーマンスにほとんど影響を与えません。
  • 直感的なAPI: 簡単なJavaScriptオブジェクトを使って状態を定義し、更新できます。
  • 非依存性: ZustandはReactの特定の機能に依存しておらず、単体の状態管理ライブラリとしても利用可能です。
  • 拡張性: ミドルウェアやカスタムロジックを簡単に統合できるため、複雑なユースケースにも対応できます。

他の状態管理ライブラリとの違い


ReduxやMobXが広く使われている一方で、Zustandは以下のようなユニークな利点があります。

  • ボイラープレートが少ない: Reduxのようなアクションやリデューサーの作成が不要で、コードが簡潔になります。
  • Context APIの欠点を補完: Context APIのように再レンダリングの影響を受けにくい設計となっています。
  • Reactに閉じない設計: ZustandはReact以外のフレームワークや純粋なJavaScriptでも使用可能です。

適用範囲


Zustandは、小規模なプロジェクトから大規模なアプリケーションまで幅広く利用できます。特に、複雑なロジックやステートの流れを効率的に管理したい場合に最適です。

Zustandのインストールと基本設定

Zustandを使用するには、まずライブラリをインストールし、基本的な設定を行う必要があります。以下にその手順を説明します。

Zustandのインストール


Zustandはnpmまたはyarnで簡単にインストールできます。以下のコマンドを実行してください。

npm install zustand
# または
yarn add zustand

基本的なストアの作成


Zustandでは、ストアを作成することで状態管理を開始します。ストアは、状態の定義とその操作方法を含む単純なJavaScript関数です。

以下に、基本的なストアの例を示します:

import create from 'zustand';

// Zustandのストアを作成
const useStore = create((set) => ({
  count: 0, // 状態の初期値
  increment: () => set((state) => ({ count: state.count + 1 })), // 状態を変更する関数
  decrement: () => set((state) => ({ count: state.count - 1 })), // 状態を減少させる関数
}));

export default useStore;

Reactコンポーネントでストアを使用


作成したストアをReactコンポーネントで利用するのも非常に簡単です。useStoreフックを呼び出して必要な状態や関数を取得します。

import React from 'react';
import useStore from './store'; // 作成したストアをインポート

const Counter = () => {
  const count = useStore((state) => state.count); // 状態を取得
  const increment = useStore((state) => state.increment); // 状態変更関数を取得
  const decrement = useStore((state) => state.decrement);

  return (
    <div>
      <h1>Count: {count}</h1>
      <button onClick={increment}>Increment</button>
      <button onClick={decrement}>Decrement</button>
    </div>
  );
};

export default Counter;

動作確認


上記のコンポーネントをアプリケーションでレンダリングすれば、カウンターが状態を管理していることを簡単に確認できます。

このように、Zustandは直感的なAPIで状態管理を始められるため、効率的な開発が可能です。次のセクションでは、状態遷移管理をどのように構築するか具体的に説明します。

状態遷移管理の基本構造

Zustandを利用した状態遷移管理は、状態の定義、状態の変更ロジック、そして状態を利用するコンポーネントの3つのステップで構成されます。このセクションでは、これらの基本構造について詳しく解説します。

状態の定義


状態はZustandのストアで定義します。以下は、シンプルな状態遷移の例です。

import create from 'zustand';

const useStore = create((set) => ({
  step: 1, // 現在のステップ
  nextStep: () => set((state) => ({ step: state.step + 1 })), // 次のステップに進む
  previousStep: () => set((state) => ({ step: state.step - 1 })), // 前のステップに戻る
  resetStep: () => set({ step: 1 }), // ステップを初期値にリセット
}));

export default useStore;

この例では、stepという状態と、状態を変更するための関数(nextSteppreviousStepresetStep)を定義しています。

状態の変更ロジック


Zustandのset関数を利用して、状態を変更するロジックを記述します。この際、現在の状態(state)に基づいて新しい状態を計算します。

  • 状態を更新する方法:
    状態を次のステップに進める関数は以下のように定義します。
  nextStep: () => set((state) => ({ step: state.step + 1 })),
  • 状態をリセットする方法:
    状態を初期値に戻す場合は、新しい値を直接セットします。
  resetStep: () => set({ step: 1 }),

コンポーネントで状態を利用


定義したストアをReactコンポーネントで使用して状態を管理します。

import React from 'react';
import useStore from './store';

const StepManager = () => {
  const step = useStore((state) => state.step); // 現在のステップを取得
  const nextStep = useStore((state) => state.nextStep); // 次のステップに進む関数を取得
  const previousStep = useStore((state) => state.previousStep);
  const resetStep = useStore((state) => state.resetStep);

  return (
    <div>
      <h1>Current Step: {step}</h1>
      <button onClick={previousStep} disabled={step === 1}>Previous Step</button>
      <button onClick={nextStep}>Next Step</button>
      <button onClick={resetStep}>Reset</button>
    </div>
  );
};

export default StepManager;

実行結果


上記のコードをReactアプリケーションに統合すると、ボタンをクリックすることで状態が更新され、それに応じて現在のステップがリアルタイムで表示されます。このように、Zustandは状態管理をシンプルかつ直感的に実装できる仕組みを提供します。

次のセクションでは、この基本構造を応用し、ステップごとの状態遷移をより高度に管理する方法を解説します。

Zustandを用いたステップごとの状態遷移の実装

ステップごとの状態遷移は、特定のプロセスやワークフローを段階的に管理する場面でよく利用されます。このセクションでは、Zustandを使った具体的な実装例を示します。

ステップ状態のストアを定義


ステップの進行状況を管理するための状態をZustandで作成します。

import create from 'zustand';

const useStepStore = create((set) => ({
  currentStep: 1, // 現在のステップ
  steps: ['Step 1: Input Data', 'Step 2: Confirm Details', 'Step 3: Complete'], // ステップ名
  nextStep: () =>
    set((state) =>
      state.currentStep < state.steps.length
        ? { currentStep: state.currentStep + 1 }
        : state
    ), // 次のステップに進む
  previousStep: () =>
    set((state) =>
      state.currentStep > 1 ? { currentStep: state.currentStep - 1 } : state
    ), // 前のステップに戻る
  resetSteps: () => set({ currentStep: 1 }), // ステップをリセット
}));

このストアでは、以下を管理します:

  • currentStep: 現在のステップ番号
  • steps: ステップの一覧
  • ステップを進める、戻る、リセットする関数

ステップ遷移コンポーネントを実装


ストアを利用して、ステップの進行状況を表示し、ボタン操作で遷移を管理するReactコンポーネントを作成します。

import React from 'react';
import useStepStore from './stepStore';

const StepManager = () => {
  const currentStep = useStepStore((state) => state.currentStep); // 現在のステップ
  const steps = useStepStore((state) => state.steps); // ステップ名
  const nextStep = useStepStore((state) => state.nextStep); // 次のステップ関数
  const previousStep = useStepStore((state) => state.previousStep); // 前のステップ関数
  const resetSteps = useStepStore((state) => state.resetSteps); // ステップリセット関数

  return (
    <div>
      <h1>{steps[currentStep - 1]}</h1>
      <p>
        Step {currentStep} of {steps.length}
      </p>
      <button onClick={previousStep} disabled={currentStep === 1}>
        Previous
      </button>
      <button onClick={nextStep} disabled={currentStep === steps.length}>
        Next
      </button>
      <button onClick={resetSteps}>Reset</button>
    </div>
  );
};

export default StepManager;

コードのポイント

  1. 状態の取得: ストアから現在のステップとステップ一覧を取得しています。
  2. 状態の更新: ボタン操作に応じてnextSteppreviousStepを呼び出します。
  3. UIの制御: ボタンの有効/無効をcurrentStepに基づいて動的に設定します。

ステップ遷移の確認


このコンポーネントをブラウザで表示すると、以下の動作が確認できます:

  • 現在のステップがタイトルとして表示されます。
  • NextPreviousボタンでステップを切り替え可能です。
  • Resetボタンで最初のステップに戻ります。

拡張例: 条件付き遷移


特定の条件でステップをスキップする場合は、nextStep関数に条件ロジックを追加します。

nextStep: () =>
  set((state) => {
    const next = state.currentStep + 1;
    if (next === 2 && !someCondition) { // 条件に応じてスキップ
      return { currentStep: next + 1 };
    }
    return next <= state.steps.length
      ? { currentStep: next }
      : state;
  }),

このように、Zustandは柔軟な状態遷移のロジックを簡単に実装できるため、複雑なアプリケーションにも対応可能です。次のセクションでは、コンポーネント間での状態共有について解説します。

コンポーネント間での状態共有

Zustandは、Reactコンポーネント間での状態共有を簡単に実現できる強力な機能を提供します。このセクションでは、Zustandを使用して複数のコンポーネント間で状態を共有し、効率的にデータを管理する方法を解説します。

複数コンポーネントでストアを利用


1つのストアを複数のコンポーネントで共有することで、状態を効率的に同期できます。以下の例では、同じステップ管理の状態を異なるコンポーネントで共有します。

import React from 'react';
import useStepStore from './stepStore'; // 既存のストアをインポート

const StepDisplay = () => {
  const currentStep = useStepStore((state) => state.currentStep); // 現在のステップを取得
  const steps = useStepStore((state) => state.steps); // ステップ名を取得
  return (
    <div>
      <h2>Current Step: {steps[currentStep - 1]}</h2>
      <p>Step {currentStep} of {steps.length}</p>
    </div>
  );
};

const StepControls = () => {
  const nextStep = useStepStore((state) => state.nextStep); // 次のステップ関数
  const previousStep = useStepStore((state) => state.previousStep); // 前のステップ関数
  const resetSteps = useStepStore((state) => state.resetSteps); // ステップリセット関数
  const currentStep = useStepStore((state) => state.currentStep);
  const steps = useStepStore((state) => state.steps);

  return (
    <div>
      <button onClick={previousStep} disabled={currentStep === 1}>
        Previous
      </button>
      <button onClick={nextStep} disabled={currentStep === steps.length}>
        Next
      </button>
      <button onClick={resetSteps}>Reset</button>
    </div>
  );
};

const App = () => (
  <div>
    <StepDisplay />
    <StepControls />
  </div>
);

export default App;

コードのポイント

  • 分割された責務:
  • StepDisplayは状態の表示に専念。
  • StepControlsは状態の更新を管理。
  • このように役割を分割することで、コードの可読性と再利用性が向上します。
  • 状態のリアルタイム同期:
    Zustandのストアを共有することで、1つのコンポーネントで状態を更新すると、他のコンポーネントに即座に反映されます。

再レンダリングの最適化


Zustandは、状態の一部のみを監視する機能を提供します。この仕組みを活用することで、不必要な再レンダリングを回避できます。

const StepDisplay = () => {
  const currentStep = useStepStore((state) => state.currentStep); // 必要な部分のみ監視
  const steps = useStepStore((state) => state.steps);
  return (
    <div>
      <h2>Current Step: {steps[currentStep - 1]}</h2>
      <p>Step {currentStep} of {steps.length}</p>
    </div>
  );
};

実行結果


上記のコードを実行すると、次のような動作が確認できます:

  • 状態の表示と制御が別のコンポーネントに分割され、それぞれが同じストアを共有して動作します。
  • ボタン操作でステップを変更すると、即座にStepDisplayコンポーネントに反映されます。

応用例


Zustandを用いて、複数のウィジェット間でフィルタ条件やユーザー設定を共有するダッシュボードアプリケーションなども容易に構築できます。

このように、Zustandは簡潔なAPIでコンポーネント間の状態共有を効率的に実現し、複雑なアプリケーションの構築にも対応します。次のセクションでは、Zustandのミドルウェアを活用して状態管理をさらに拡張する方法を紹介します。

Zustandのミドルウェアを活用した拡張性

Zustandのミドルウェア機能を利用すると、状態管理にさらなる機能を追加できます。ミドルウェアは、ストアの振る舞いを拡張するための便利なツールであり、状態の永続化やデバッグ、ロギングなどに役立ちます。このセクションでは、代表的なミドルウェアの利用方法を解説します。

ミドルウェアの基本概念


Zustandのミドルウェアは、ストアをラップしてその機能を拡張します。ミドルウェアを追加することで、以下のような機能を簡単に実装できます:

  • 状態の永続化: ローカルストレージやセッションストレージに状態を保存。
  • ロギング: 状態の変更時にその内容を記録。
  • デバッグ: 状態遷移を詳細に追跡。

状態の永続化


状態をローカルストレージに保存し、ページリロード後も状態を保持するには、persistミドルウェアを使用します。

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

const usePersistedStore = create(
  persist(
    (set) => ({
      count: 0,
      increment: () => set((state) => ({ count: state.count + 1 })),
      decrement: () => set((state) => ({ count: state.count - 1 })),
    }),
    {
      name: 'counter-storage', // ストレージキー名
    }
  )
);

export default usePersistedStore;

このストアを利用すると、状態がローカルストレージに保存されます。ページをリロードしても保存された状態を復元できます。

状態のロギング


状態変更時にログを記録するには、loggerミドルウェアを自作して組み込むことができます。

const logger = (config) => (set, get, api) =>
  config(
    (args) => {
      console.log('Previous state:', get());
      set(args);
      console.log('Next state:', get());
    },
    get,
    api
  );

const useLoggedStore = create(
  logger((set) => ({
    count: 0,
    increment: () => set((state) => ({ count: state.count + 1 })),
    decrement: () => set((state) => ({ count: state.count - 1 })),
  }))
);

export default useLoggedStore;

状態が更新されるたびに、前後の状態がコンソールに記録されます。

デバッグの強化


Zustandは、開発中に状態を追跡しやすくするためにRedux DevToolsとの連携をサポートしています。reduxミドルウェアを使用すると、Zustandの状態遷移をRedux DevToolsで可視化できます。

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

const useDebuggableStore = create(
  devtools((set) => ({
    count: 0,
    increment: () => set((state) => ({ count: state.count + 1 })),
    decrement: () => set((state) => ({ count: state.count - 1 })),
  }))
);

export default useDebuggableStore;

このストアを使えば、状態の変更をRedux DevToolsで確認でき、デバッグが格段に効率化されます。

応用例: 複数ミドルウェアの組み合わせ


複数のミドルウェアを組み合わせることで、さらなる柔軟性を持たせることが可能です。

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

const useEnhancedStore = create(
  devtools(
    persist(
      (set) => ({
        count: 0,
        increment: () => set((state) => ({ count: state.count + 1 })),
        decrement: () => set((state) => ({ count: state.count - 1 })),
      }),
      { name: 'enhanced-storage' }
    )
  )
);

export default useEnhancedStore;

この例では、状態が永続化されるとともに、Redux DevToolsで状態遷移を追跡できます。

実行結果

  • 状態の永続化によって、リロード後も状態が保持される。
  • ロギング機能で状態変更の追跡が可能。
  • Redux DevToolsで状態の変化を視覚的に確認できる。

Zustandのミドルウェアは、シンプルな状態管理を超えて柔軟で強力な機能を追加するための重要なツールです。次のセクションでは、実際のアプリケーション例として、Zustandを活用したTodoアプリでのステップ管理を解説します。

実装例:Todoアプリでのステップ管理

Zustandを使用すると、Todoアプリのステップごとの状態管理をシンプルに実装できます。このセクションでは、Zustandを活用して、タスクの進捗を段階的に管理するアプリケーションの具体例を紹介します。

アプリの要件


このTodoアプリでは、以下の機能を実装します:

  1. タスクのリスト管理(追加、削除)。
  2. 各タスクの状態(未完了、進行中、完了)をステップごとに管理。
  3. ステップごとの進捗状況を可視化。

ストアの設計


Zustandを用いてタスクとそのステップ状態を管理するストアを作成します。

import create from 'zustand';

const useTodoStore = create((set) => ({
  todos: [], // タスクのリスト
  addTodo: (title) =>
    set((state) => ({
      todos: [...state.todos, { id: Date.now(), title, step: 'not_started' }],
    })), // タスクを追加
  removeTodo: (id) =>
    set((state) => ({
      todos: state.todos.filter((todo) => todo.id !== id),
    })), // タスクを削除
  updateStep: (id, step) =>
    set((state) => ({
      todos: state.todos.map((todo) =>
        todo.id === id ? { ...todo, step } : todo
      ),
    })), // タスクの状態を更新
}));

UIコンポーネントの構築


タスクリストの表示とステップ管理を行うUIコンポーネントを作成します。

import React, { useState } from 'react';
import useTodoStore from './todoStore';

const TodoApp = () => {
  const [newTodo, setNewTodo] = useState(''); // 新しいタスクの入力
  const todos = useTodoStore((state) => state.todos); // タスクリストを取得
  const addTodo = useTodoStore((state) => state.addTodo); // タスクを追加
  const removeTodo = useTodoStore((state) => state.removeTodo); // タスクを削除
  const updateStep = useTodoStore((state) => state.updateStep); // タスクの状態を更新

  const handleAddTodo = () => {
    if (newTodo.trim()) {
      addTodo(newTodo);
      setNewTodo('');
    }
  };

  return (
    <div>
      <h1>Todo App with Step Management</h1>
      <input
        type="text"
        value={newTodo}
        onChange={(e) => setNewTodo(e.target.value)}
        placeholder="Enter a new task"
      />
      <button onClick={handleAddTodo}>Add Task</button>

      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>
            <span>{todo.title}</span>
            <select
              value={todo.step}
              onChange={(e) => updateStep(todo.id, e.target.value)}
            >
              <option value="not_started">Not Started</option>
              <option value="in_progress">In Progress</option>
              <option value="completed">Completed</option>
            </select>
            <button onClick={() => removeTodo(todo.id)}>Remove</button>
          </li>
        ))}
      </ul>
    </div>
  );
};

export default TodoApp;

進捗状況の可視化


タスクのステータスを集計して進捗状況を表示するコンポーネントを追加します。

const Progress = () => {
  const todos = useTodoStore((state) => state.todos);
  const total = todos.length;
  const completed = todos.filter((todo) => todo.step === 'completed').length;

  return (
    <div>
      <h2>Progress</h2>
      <p>
        Completed: {completed} / {total}
      </p>
    </div>
  );
};

全体の統合


アプリケーション全体を統合して動作を確認します。

const App = () => (
  <div>
    <TodoApp />
    <Progress />
  </div>
);

export default App;

実行結果

  • 新しいタスクを追加できる。
  • 各タスクの状態を「未開始」「進行中」「完了」から選択して変更できる。
  • 進捗状況がリアルタイムで反映される。

応用例


この実装をベースに、以下のような機能を追加できます:

  • タスクの優先度管理。
  • 締め切りやアラート機能の実装。
  • 状態の永続化(ミドルウェアpersistを利用)。

このように、Zustandを使用すると、Todoアプリのような実用的なプロジェクトでも、簡潔で拡張性のある状態管理が可能です。次のセクションでは、Zustandを使用する際に直面する可能性のあるエラーとその解決方法を解説します。

よくあるエラーとその解決方法

Zustandを使用する際には、特定のエラーや問題に遭遇することがあります。このセクションでは、よくあるエラーの原因とその解決方法について解説します。

1. Zustandの状態が更新されない


原因:
状態更新関数(set)の使用方法に誤りがある可能性があります。特に、状態の直接変更を試みるケースが原因となります。

:

const useStore = create((set) => ({
  count: 0,
  increment: () => {
    // 状態を直接変更しようとする(誤り)
    set((state) => {
      state.count++;
      return state;
    });
  },
}));

解決方法:
状態はイミュータブルである必要があります。新しい状態オブジェクトを返すように修正します。

const useStore = create((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })), // 正しい例
}));

2. 状態の変更がコンポーネントに反映されない


原因:

  • 必要な状態ではなく、ストア全体を取得している可能性があります。
  • コンポーネントが不要な再レンダリングを回避している場合もあります。

:

const count = useStore(); // ストア全体を取得(誤り)

解決方法:
ストア全体ではなく、必要な状態のみを選択する関数を引数として渡します。

const count = useStore((state) => state.count); // 必要な部分のみ取得

3. ミドルウェア使用時の設定エラー


原因:
ミドルウェア(persistdevtoolsなど)の設定が正しくない場合に発生します。例えば、persistでストレージキー名を設定し忘れることがあります。

:

const useStore = create(
  persist((set) => ({
    count: 0,
    increment: () => set((state) => ({ count: state.count + 1 })),
  }))
);

解決方法:
ミドルウェアのオプションを正しく設定します。

const useStore = create(
  persist(
    (set) => ({
      count: 0,
      increment: () => set((state) => ({ count: state.count + 1 })),
    }),
    { name: 'count-storage' } // ストレージキー名を指定
  )
);

4. 状態が初期化されない


原因:
状態の初期値を設定していない、または誤った方法で初期値をセットしている場合に発生します。

:

const useStore = create((set) => ({
  increment: () => set((state) => ({ count: state.count + 1 })),
}));

解決方法:
初期値を必ず設定します。

const useStore = create((set) => ({
  count: 0, // 初期値を設定
  increment: () => set((state) => ({ count: state.count + 1 })),
}));

5. Redux DevToolsで状態が表示されない


原因:
devtoolsミドルウェアが正しく適用されていない場合に発生します。

解決方法:
以下のようにdevtoolsを正しく適用します。

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

const useStore = create(
  devtools((set) => ({
    count: 0,
    increment: () => set((state) => ({ count: state.count + 1 })),
  }))
);

6. 状態が複数のコンポーネントで正しく共有されない


原因:
ストアの定義が複数回実行されている可能性があります。例えば、同じストアを異なるモジュールやファイルで別々に定義している場合です。

:

// ファイルA
export const useStoreA = create((set) => ({ count: 0 }));

// ファイルB
export const useStoreB = create((set) => ({ count: 0 }));

解決方法:
ストアは単一のファイルで定義し、必要に応じてインポートします。

// store.js
const useStore = create((set) => ({ count: 0 }));
export default useStore;

// コンポーネント内で使用
import useStore from './store';

まとめ


Zustandは直感的で使いやすい状態管理ライブラリですが、誤った使い方をするとエラーが発生します。本セクションで解説したエラーと解決方法を参考に、Zustandの導入と運用をスムーズに進めてください。次のセクションでは、記事のまとめとして、Zustandを使った効率的な状態管理のポイントを振り返ります。

まとめ

本記事では、ReactでZustandを使用した状態管理の方法について解説しました。Zustandの概要から始まり、インストールや基本設定、ステップごとの状態遷移管理、ミドルウェアの活用、そして実践的なTodoアプリの例まで幅広く紹介しました。さらに、よくあるエラーとその解決方法も取り上げ、実用性を高めました。

Zustandの直感的で柔軟なAPIは、小規模なプロジェクトから複雑なアプリケーションまで、あらゆるユースケースで役立ちます。状態管理の煩雑さを軽減し、開発者がロジックに集中できる環境を提供する点で、Zustandは非常に優れたツールです。本記事を参考に、Zustandを活用した効率的なReactアプリケーションの構築に挑戦してください。

コメント

コメントする

目次