Zustandで簡単に実現!Reactの持続的状態管理の実践例

Reactでのアプリケーション開発では、状態管理は非常に重要な課題です。特に、ユーザーの設定やアプリのセッションデータを永続化する必要がある場合、効率的で簡単な手法が求められます。この記事では、軽量で柔軟性の高い状態管理ライブラリ「Zustand」を用いて、持続的状態管理(Persist)の実装方法を解説します。具体的な手順と実践例を通じて、Zustandを活用した効率的な状態管理の方法を学びましょう。

目次

Zustandの基本概要


Zustandは、Reactアプリケーションのための軽量かつシンプルな状態管理ライブラリです。その設計は、ボイラープレートコードを最小限に抑え、直感的なAPIで簡単に状態を管理できるようにされています。Reduxのような強力な機能を提供しつつも、学習コストが低い点が大きな特徴です。

Zustandの主な特徴

  • 軽量: Zustandのインストールサイズはわずか数KBで、プロジェクトに負担をかけません。
  • 直感的なAPI: 状態管理の実装が簡単で、コードが読みやすい。
  • 依存性が低い: 他の状態管理ツールに比べて依存するライブラリが少なく、柔軟性が高い。
  • パフォーマンス: Reactコンポーネントの再レンダリングを最小限に抑える設計。

他の状態管理ツールとの比較


ZustandはReduxやMobXのようなツールに比べ、次のような利点があります。

  1. 状態管理のためのシンプルな構造を持ち、Reduxのようなアクションやリデューサーを定義する必要がありません。
  2. Context APIを使用せずに状態をグローバルに共有でき、パフォーマンスの問題を回避できます。

これにより、Zustandは特に小規模から中規模のアプリケーションや、プロトタイプ開発に適した選択肢となっています。

Zustandでの基本的な状態管理の実装


Zustandを使用すると、簡単なコードで効率的な状態管理が可能です。以下では、Zustandを使った基本的な状態管理の実装例を紹介します。

ステップ1: Zustandのインストール


まずはZustandをインストールします。以下のコマンドをターミナルで実行してください。

npm install zustand

ステップ2: 基本的な状態管理のセットアップ


次に、Zustandを使用して状態管理ストアを作成します。例として、カウント機能を持つシンプルなストアを実装します。

import create from 'zustand';

const useStore = create((set) => ({
  count: 0, // 状態の初期値
  increment: () => set((state) => ({ count: state.count + 1 })), // 状態を変更する関数
  decrement: () => set((state) => ({ count: state.count - 1 })), // 状態を変更する関数
}));

export default useStore;

ステップ3: ストアをReactコンポーネントで利用


作成したストアをReactコンポーネント内で使用します。useStoreフックを使って状態を取得および操作できます。

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

function Counter() {
  const { count, increment, decrement } = useStore();

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

export default Counter;

コードの説明

  • 状態の初期値: countが初期値として0に設定されています。
  • 状態変更関数: incrementdecrementは状態を変更するための関数です。set関数を用いて状態を更新します。
  • コンポーネントでの利用: useStoreフックを使って状態と操作関数を取得し、ボタンのクリックイベントで呼び出しています。

この基本的なストアの構造を基に、アプリケーションのニーズに応じた状態管理を簡単に拡張できます。次に、状態を持続化する方法について詳しく見ていきます。

Zustand Persistの設定と使用方法


ZustandのPersist機能を利用することで、状態をローカルストレージやセッションストレージに保存し、アプリケーションが再読み込みされた後でも状態を保持できます。以下では、Zustand Persistのセットアップ方法と基本的な使用例を解説します。

ステップ1: Zustand Middlewareのインストール


Persist機能を使うためには、Zustandのミドルウェアが必要です。以下のコマンドでインストールしてください。

npm install zustand/middleware

ステップ2: Zustand Persistの設定


ストアを作成する際に、persistミドルウェアを使用して状態を永続化します。以下は例です。

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

const useStore = create(
  persist(
    (set) => ({
      count: 0, // 状態の初期値
      increment: () => set((state) => ({ count: state.count + 1 })),
      decrement: () => set((state) => ({ count: state.count - 1 })),
    }),
    {
      name: 'counter-storage', // ローカルストレージのキー名
      getStorage: () => localStorage, // 保存先を指定(デフォルトはローカルストレージ)
    }
  )
);

export default useStore;

ステップ3: コンポーネントでの利用


先ほどと同様に、useStoreフックを使って状態を操作します。状態が永続化されるため、ページをリロードしても値は維持されます。

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

function Counter() {
  const { count, increment, decrement } = useStore();

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

export default Counter;

コードの説明

  • persistミドルウェア: 状態をローカルストレージやセッションストレージに保存するための機能です。
  • nameオプション: 保存時に使用されるキー名を指定します。
  • getStorageオプション: 保存先を指定します。localStoragesessionStorageが選択可能です。

動作確認


アプリケーションを起動し、カウントの値を変更後にブラウザをリロードしてください。値が保存され、リロード後も同じ状態が維持されていることが確認できます。

このように、Zustand Persistを使用することで、シンプルかつ効率的に状態の永続化を実現できます。次に、保存先や高度な設定オプションについて詳しく説明します。

ローカルストレージを用いた状態の保存


Zustandのpersistミドルウェアを使用することで、状態をローカルストレージに保存し、アプリケーションをリロードした後でも状態を維持できます。ここでは、ローカルストレージを利用した具体的な保存方法と、実装上の注意点を解説します。

ローカルストレージを使った状態保存の実装


以下は、ローカルストレージを利用した状態保存のコード例です。

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

const useStore = create(
  persist(
    (set) => ({
      user: null, // 状態の初期値
      login: (userData) => set({ user: userData }), // ユーザー情報を更新
      logout: () => set({ user: null }), // ユーザー情報をクリア
    }),
    {
      name: 'user-storage', // ローカルストレージのキー名
      getStorage: () => localStorage, // 保存先を指定
    }
  )
);

export default useStore;

この例では、userという状態をローカルストレージに保存し、ユーザーのログイン情報を保持します。

保存した状態の確認


以下の手順で、状態がローカルストレージに保存されていることを確認できます。

  1. ブラウザの開発者ツールを開く。
  2. Applicationタブを選択し、Local Storageを開く。
  3. 保存したキー(例: user-storage)が存在し、その値がJSON形式で保存されていることを確認します。

ローカルストレージのデータフォーマット


ローカルストレージには、状態が次のようなJSON形式で保存されます。

{
  "state": {
    "user": {
      "name": "John Doe",
      "email": "john@example.com"
    }
  },
  "version": 0
}
  • state: 現在の状態が格納されています。
  • version: 状態管理のバージョン(カスタム設定が可能)。

実装上の注意点

  1. ストレージ容量: ローカルストレージには保存容量の制限(通常は約5MB)があるため、大量のデータを保存する用途には不向きです。
  2. 機密データの取り扱い: ローカルストレージは暗号化されていないため、機密性の高いデータ(例: パスワード、トークンなど)の保存は避けてください。必要に応じて暗号化を実装してください。
  3. データの同期性: 複数のタブでアプリを開いている場合、ローカルストレージのデータ変更がリアルタイムに反映されない可能性があります。この場合、storageイベントを利用して対応できます。

ストレージのクリア方法


状態をリセットするには、次のようにlogout関数を呼び出すか、ブラウザのローカルストレージを直接クリアします。

useStore.getState().logout(); // 状態をリセット
localStorage.removeItem('user-storage'); // ローカルストレージをクリア

まとめ


ローカルストレージを用いることで、簡単に状態を永続化できます。ユーザー体験を向上させるために、適切なデータを選んで保存することが重要です。次は、Zustand Persistを活用した高度な設定について解説します。

Zustand Persistの高度な設定オプション


Zustandのpersistミドルウェアには、状態保存の細かな挙動を制御できる高度な設定オプションがあります。これにより、特定の要件や複雑なシナリオに対応することが可能です。以下では、主要なオプションとその活用例について解説します。

高度なオプション一覧

  1. name
    ローカルストレージまたはセッションストレージに保存する際のキー名を指定します。
   name: 'custom-key'
  1. getStorage
    保存先を指定します。デフォルトではlocalStorageですが、sessionStorageやカスタムストレージ(例: IndexedDB)にも対応できます。
   getStorage: () => sessionStorage
  1. serialize
    保存時にデータを変換する関数を指定します。デフォルトはJSON.stringifyです。暗号化が必要な場合に便利です。
   serialize: (state) => btoa(JSON.stringify(state)) // Base64エンコード
  1. deserialize
    読み込み時にデータを変換する関数を指定します。デフォルトはJSON.parseです。
   deserialize: (str) => JSON.parse(atob(str)) // Base64デコード
  1. partialize
    永続化する状態の一部を選択できます。状態が大きくなる場合に有効です。
   partialize: (state) => ({ user: state.user }) // userのみ保存
  1. onRehydrateStorage
    保存された状態が読み込まれる際に実行されるコールバック関数を指定します。
   onRehydrateStorage: () => (state) => {
     console.log('State rehydrated', state);
   }

実装例: 高度な設定を使用したZustand Persist


以下は、これらのオプションを組み合わせた高度な設定例です。

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

const useStore = create(
  persist(
    (set) => ({
      user: null,
      preferences: { theme: 'light' },
      login: (userData) => set({ user: userData }),
      logout: () => set({ user: null }),
    }),
    {
      name: 'app-storage',
      getStorage: () => localStorage, // ローカルストレージに保存
      serialize: (state) => btoa(JSON.stringify(state)), // Base64エンコード
      deserialize: (str) => JSON.parse(atob(str)), // Base64デコード
      partialize: (state) => ({ user: state.user }), // userのみ保存
      onRehydrateStorage: () => (state) => {
        console.log('Storage rehydrated:', state);
      },
    }
  )
);

export default useStore;

高度な設定を使う際の注意点

  1. データサイズの最適化
    状態の一部だけを保存する場合、partializeを利用して不要なデータの保存を避けましょう。
  2. セキュリティ
    暗号化が必要なデータ(例: ユーザーセッション)を保存する場合は、serializeで暗号化を実装してください。
  3. エラーハンドリング
    カスタムストレージやシリアライズ処理でエラーが発生した場合に備え、適切なデバッグ手段を用意しましょう。

まとめ


Zustand Persistの高度な設定を活用することで、アプリケーションに適した状態管理を柔軟に実現できます。次は、これらの機能を活用した具体的な実践例について詳しく説明します。

実践例:Zustandを用いたタスクリストアプリ


ここでは、ZustandとPersistを活用して、状態が永続化されるタスクリストアプリを構築します。このアプリでは、タスクの追加、削除、完了状態の切り替えをサポートします。

ステップ1: Zustandストアの作成


タスク管理用のストアを作成します。Persistを利用してタスクの状態をローカルストレージに保存します。

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

const useTaskStore = create(
  persist(
    (set) => ({
      tasks: [], // タスクの初期状態
      addTask: (task) =>
        set((state) => ({
          tasks: [...state.tasks, { id: Date.now(), text: task, completed: false }],
        })), // タスクを追加
      toggleTask: (id) =>
        set((state) => ({
          tasks: state.tasks.map((task) =>
            task.id === id ? { ...task, completed: !task.completed } : task
          ),
        })), // タスクの完了状態を切り替え
      deleteTask: (id) =>
        set((state) => ({
          tasks: state.tasks.filter((task) => task.id !== id),
        })), // タスクを削除
    }),
    {
      name: 'task-storage', // ローカルストレージのキー名
      getStorage: () => localStorage, // ローカルストレージを指定
    }
  )
);

export default useTaskStore;

ステップ2: タスク管理コンポーネントの実装


タスクの表示、追加、削除、状態切り替えを行うコンポーネントを作成します。

import React, { useState } from 'react';
import useTaskStore from './taskStore';

function TaskApp() {
  const { tasks, addTask, toggleTask, deleteTask } = useTaskStore();
  const [taskText, setTaskText] = useState('');

  const handleAddTask = () => {
    if (taskText.trim()) {
      addTask(taskText);
      setTaskText('');
    }
  };

  return (
    <div>
      <h1>Task List</h1>
      <div>
        <input
          type="text"
          value={taskText}
          onChange={(e) => setTaskText(e.target.value)}
          placeholder="Add a new task"
        />
        <button onClick={handleAddTask}>Add Task</button>
      </div>
      <ul>
        {tasks.map((task) => (
          <li key={task.id} style={{ textDecoration: task.completed ? 'line-through' : 'none' }}>
            {task.text}
            <button onClick={() => toggleTask(task.id)}>Toggle</button>
            <button onClick={() => deleteTask(task.id)}>Delete</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default TaskApp;

ステップ3: アプリケーションの起動


上記のコンポーネントをReactアプリのエントリーポイントに組み込み、アプリを起動します。

import React from 'react';
import ReactDOM from 'react-dom';
import TaskApp from './TaskApp';

ReactDOM.render(
  <React.StrictMode>
    <TaskApp />
  </React.StrictMode>,
  document.getElementById('root')
);

動作確認

  1. アプリを起動し、タスクを追加します。
  2. タスクを完了(Toggleボタン)または削除(Deleteボタン)して、動作を確認します。
  3. ページをリロードしてもタスクが保存されていることを確認します(ローカルストレージに状態が保存されます)。

アプリケーションでの工夫

  • UI改善: 完了タスクを別リストで表示する、フィルター機能を追加するなど。
  • 高度な永続化: IndexedDBや外部APIと連携してデータを永続化する。

まとめ


Zustandを使うことで、シンプルで効率的な状態管理を実現し、アプリの状態をローカルストレージに簡単に永続化できます。この例を応用して、他の用途にも適用できる柔軟な状態管理を構築できます。次に、パフォーマンス最適化のポイントを詳しく解説します。

パフォーマンス最適化のポイント


Zustandを用いた状態管理は、シンプルさと効率性が特徴ですが、アプリケーションの規模が大きくなるとパフォーマンスの課題が発生することもあります。ここでは、Zustand Persistを使用した場合のパフォーマンス最適化のポイントを解説します。

1. 状態のスコープを適切に分割する


一つのストアにすべての状態をまとめてしまうと、状態が変更されるたびに多くのコンポーネントが再レンダリングされる可能性があります。
解決策: 状態を異なるストアに分割し、必要な部分だけを管理します。

// タスク管理用ストア
const useTaskStore = create((set) => ({
  tasks: [],
  addTask: (task) => set((state) => ({ tasks: [...state.tasks, task] })),
}));

// ユーザー管理用ストア
const useUserStore = create((set) => ({
  user: null,
  setUser: (user) => set({ user }),
}));

2. 必要な状態だけを選択する


ZustandのuseStoreフックでは、状態の一部だけを選択して使用できます。これにより、不要な再レンダリングを防ぎます。

import { useStore } from './store';

// 選択した状態だけを取得
const taskCount = useStore((state) => state.tasks.length);

3. Zustandの`partialize`オプションを活用する


状態の一部だけを永続化することで、ストレージへの書き込み量を減らし、パフォーマンスを向上させます。

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

const useStore = create(
  persist(
    (set) => ({
      user: { name: 'John', email: 'john@example.com' },
      preferences: { theme: 'dark' },
    }),
    {
      name: 'user-storage',
      partialize: (state) => ({ preferences: state.preferences }), // preferencesのみ保存
    }
  )
);

4. ストレージ操作の最適化


ローカルストレージやセッションストレージへの頻繁なアクセスは、パフォーマンスに影響を与えることがあります。
解決策: 状態の変更頻度を制御するミドルウェア(例: debounce)を追加します。

const debouncedPersist = (set) => {
  let timeout;
  return (nextState) => {
    clearTimeout(timeout);
    timeout = setTimeout(() => set(nextState), 200); // 200msの遅延
  };
};

const useStore = create(
  persist(
    (set) => ({
      data: '',
      updateData: (newData) => set({ data: newData }),
    }),
    {
      name: 'data-storage',
      middleware: debouncedPersist,
    }
  )
);

5. レンダリング最適化


ストアの状態が変更されても、UIの一部だけが更新されるように設計することが重要です。
解決策: Zustandのsubscribeを利用して、コンポーネント外で状態変更を監視します。

const useTaskStore = create((set) => ({
  tasks: [],
  addTask: (task) => set((state) => ({ tasks: [...state.tasks, task] })),
}));

// 外部で状態を監視
useTaskStore.subscribe((state) => {
  console.log('Tasks updated:', state.tasks);
});

6. IndexedDBや外部ストレージの活用


ローカルストレージにはサイズ制限があるため、データ量が多い場合はIndexedDBなどの外部ストレージを利用することでパフォーマンスを向上させられます。

import { openDB } from 'idb';

const db = await openDB('app-database', 1, {
  upgrade(db) {
    db.createObjectStore('tasks');
  },
});

// Zustandと統合
const useStore = create(
  persist(
    (set) => ({
      tasks: [],
      addTask: (task) => set((state) => ({ tasks: [...state.tasks, task] })),
    }),
    {
      name: 'task-storage',
      getStorage: () => db,
    }
  )
);

まとめ


Zustandを用いたアプリケーションでのパフォーマンスを最適化するには、状態のスコープ分割、適切な状態の選択、ストレージ操作の効率化などが重要です。これらのテクニックを活用することで、軽量かつスムーズなアプリケーションを構築できます。次に、トラブルシューティングとよくあるエラーについて解説します。

トラブルシューティングとよくあるエラー


Zustandを使用した状態管理はシンプルで効率的ですが、Persistを含む高度な機能を実装する際には、いくつかの一般的なエラーや問題が発生することがあります。ここでは、それらの問題と解決策を紹介します。

1. 状態が正しく永続化されない


症状: ローカルストレージにデータが保存されない、または保存されたデータがリロード後に反映されない。
原因: persistミドルウェアの設定ミス、またはgetStorageオプションの指定漏れ。
解決策:

  • getStorageオプションで正しいストレージを指定してください。
  • nameオプションでユニークなキー名を設定してください。
const useStore = create(
  persist(
    (set) => ({
      count: 0,
      increment: () => set((state) => ({ count: state.count + 1 })),
    }),
    {
      name: 'counter-storage', // ローカルストレージのキー名
      getStorage: () => localStorage,
    }
  )
);

2. JSONのパースエラー


症状: アプリケーション起動時にJSON.parseエラーが発生する。
原因: ローカルストレージに破損したデータが保存されている場合、またはserialize/deserializeの不一致。
解決策:

  • ローカルストレージのデータをクリアしてください。
  • serializedeserializeの処理が対応していることを確認してください。
deserialize: (str) => JSON.parse(atob(str)), // Base64デコード
serialize: (state) => btoa(JSON.stringify(state)), // Base64エンコード

3. 状態が期待通りに更新されない


症状: 状態を更新しても、変更が反映されない。
原因: 状態の更新が正しく設定されていない、またはset関数のミス。
解決策:

  • 状態更新のロジックを確認し、set関数の書き方が正しいか検証してください。
set((state) => ({ tasks: [...state.tasks, newTask] }));

4. 複数タブで状態が同期されない


症状: 同じアプリケーションを複数タブで開いている場合に、状態が同期されない。
原因: ローカルストレージではリアルタイム同期がサポートされていない。
解決策:

  • storageイベントを使用して状態を手動で同期します。
window.addEventListener('storage', () => {
  useStore.persist.rehydrate(); // Zustandの状態を再読み込み
});

5. 状態の初期化が正しく行われない


症状: 状態が正しく初期化されず、空のオブジェクトや配列が返される。
原因: onRehydrateStorageフックを使用していない、または初期化ロジックの欠如。
解決策:

  • onRehydrateStorageを活用して、デフォルトの初期化ロジックを実装してください。
onRehydrateStorage: () => (state) => {
  if (!state) {
    return { tasks: [] }; // 状態のデフォルト値
  }
},

6. IndexedDBや外部ストレージとの統合時のエラー


症状: IndexedDBを使用する場合、ストレージの初期化エラーやデータ書き込みエラーが発生する。
原因: IndexedDBの操作が非同期であるため、初期化が遅延する可能性。
解決策:

  • 非同期操作をPromiseでラップし、初期化が完了するまで待機するロジックを追加してください。
const db = await openDB('app-database', 1, {
  upgrade(db) {
    db.createObjectStore('tasks');
  },
});

7. 再レンダリングが多発する


症状: 状態の変更で過剰な再レンダリングが発生する。
原因: 不要な状態が選択されている、または状態のスコープが広すぎる。
解決策:

  • 必要な状態だけを選択して使用する。
const count = useStore((state) => state.count); // 必要な部分のみ取得

まとめ


Zustand Persistを使用する際に遭遇する可能性のある問題とその解決策を解説しました。これらのトラブルシューティング方法を理解しておけば、スムーズなアプリケーション開発が可能になります。次は、記事のまとめです。

まとめ


本記事では、Zustandを用いた持続的状態管理(Persist)の実践例と、それを活用する際のパフォーマンス最適化やトラブルシューティングについて解説しました。Zustandは、軽量で柔軟性の高い状態管理ライブラリであり、Persist機能を活用することで、ユーザー体験を向上させる持続的状態管理を簡単に実現できます。

具体的には、ローカルストレージやセッションストレージを用いたデータ永続化、パフォーマンス向上のための部分的な状態管理、エラー対処法などを紹介しました。これらの知識を活用することで、効率的で堅牢なReactアプリケーションを構築できます。

次のプロジェクトでZustandを活用し、さらに快適な開発環境を実現してください。

コメント

コメントする

目次