React小規模プロジェクトに最適!Zustandでの状態管理実装方法

小規模なReactプロジェクトにおいて、状態管理は重要な課題です。しかし、大規模なライブラリや複雑なセットアップが不要なケースでは、もっと軽量でシンプルなソリューションが求められます。ここで注目すべきなのが、Zustandです。この軽量な状態管理ライブラリは、直感的で柔軟なAPIを備え、小規模プロジェクトや特定のユースケースに最適です。本記事では、Zustandの基本的な使い方から応用例までを解説し、効率的な状態管理の方法を学びます。これにより、React開発におけるワークフローが格段に改善されるでしょう。

目次

状態管理ライブラリZustandとは


Zustandは、React用の軽量で直感的な状態管理ライブラリです。その名はドイツ語で「状態」を意味し、そのシンプルさと柔軟性から多くの開発者に支持されています。ReduxやMobXといった他の状態管理ライブラリに比べ、コード量が少なく、学習コストが低いのが特徴です。

Zustandの特徴

  • 軽量で高速: Zustandは依存関係が少なく、アプリケーションのパフォーマンスにほとんど影響を与えません。
  • 使いやすいAPI: 状態の作成、取得、更新が簡単に行えます。
  • フレキシブルな設計: Reactのコンテキストを使用しないため、必要に応じて他の状態管理方法と併用できます。
  • ネストした状態管理: 複雑な状態も簡単に管理できるよう設計されています。

ZustandがReactプロジェクトに向いている理由


Reactは元々状態管理の仕組みを持っていますが、コンポーネント間での状態共有やロジックの複雑化が進むと管理が困難になります。Zustandを使用すると、こうした問題をシンプルかつ効果的に解決できます。

Zustandは、特に小規模から中規模プロジェクトでの使用に最適であり、Reactの機能を最大限に活かすための強力なツールと言えるでしょう。

なぜ小規模プロジェクトに適しているのか

Zustandは、シンプルで軽量な設計により、小規模プロジェクトでの状態管理に特化した機能を提供します。他の状態管理ライブラリと比較して、セットアップや運用が簡単で、過剰な機能に悩まされることがありません。以下にその理由を詳しく解説します。

シンプルで軽量


Zustandは、わずか数行のコードで状態管理を実現できます。Reduxのように複数のアクションやリデューサーを定義する必要がなく、状態の定義と操作が直感的です。また、依存関係が少ないため、プロジェクトのビルドサイズを抑えられます。

柔軟性と迅速な開発


小規模プロジェクトでは、短期間での開発とシンプルな構造が求められます。Zustandの柔軟なAPIは、必要最小限のコードで状態を定義し、すぐに使用できるため、迅速な開発が可能です。

初期設定の容易さ


Zustandはセットアップが非常に簡単で、他のライブラリのように複雑な設定ファイルや特定の構造を強制されません。これにより、初心者でも扱いやすく、プロジェクト開始時の負担を軽減します。

Reactコンテキストを使用しない


ZustandはReactのコンテキストに依存せず、独自の仕組みで状態管理を行います。このため、コンポーネント間の状態共有が効率的に行え、再レンダリングのコストを最小限に抑えられます。

他のライブラリとの比較

  • Redux: Reduxは大規模プロジェクトで効果的ですが、小規模プロジェクトではその複雑さが負担となる場合があります。
  • MobX: MobXは柔軟性が高いですが、学習コストが高くなることがあるため、シンプルなプロジェクトにはやや不向きです。
  • React Context: React Contextは簡単に状態を共有できますが、大量の状態管理や頻繁な更新には適していません。

Zustandは、これらのライブラリに比べて学習コストが低く、必要最小限の機能を提供するため、小規模プロジェクトに最適な選択肢といえます。

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

ZustandをReactプロジェクトに導入する手順は非常にシンプルで、初心者でも簡単に始められます。このセクションでは、Zustandのインストール方法と基本的なセットアップについて説明します。

インストール


Zustandはnpmまたはyarnを使って簡単にインストールできます。以下のコマンドをプロジェクトのルートディレクトリで実行してください。

npmを使用する場合

npm install zustand

yarnを使用する場合

yarn add zustand

インストールが完了したら、Zustandを使う準備が整います。

基本的なセットアップ


Zustandで状態管理を開始するには、状態の定義とストア(store)の作成が必要です。以下の例は、シンプルなカウンター機能を実装したものです。

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;

このコードでは、create関数を使用して状態ストアを定義しています。set関数を利用して、状態を簡単に更新できます。

コンポーネントでの使用方法


定義したストアをReactコンポーネント内で使用するには、カスタムフックを呼び出します。

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

function Counter() {
  const { count, increment, decrement } = useStore(); // 状態と更新関数を取得

  return (
    <div>
      <h1>{count}</h1>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
    </div>
  );
}

export default Counter;

この例では、状態ストアからcountと状態を操作するincrementdecrement関数を取得し、それをReactコンポーネント内で利用しています。

まとめ


以上の手順で、Zustandをプロジェクトにインストールし、基本的な状態管理のセットアップが完了します。このようにZustandは最小限のコードで状態管理を実現でき、シンプルな構造が特徴です。次のステップでは、複数のコンポーネント間で状態を共有する方法を紹介します。

状態管理の基本的な構造

Zustandを用いた状態管理は、直感的かつシンプルな構造で行えます。このセクションでは、Zustandを使用して状態を定義し、操作する基本的な方法を具体例とともに解説します。

状態の定義


Zustandでは、状態は関数内で定義されます。この関数は、初期状態とそれを更新するロジックを指定します。

import create from 'zustand';

// Zustandの状態ストア
const useStore = create((set) => ({
  count: 0, // 初期状態
  increment: () => set((state) => ({ count: state.count + 1 })), // 状態を1増加
  decrement: () => set((state) => ({ count: state.count - 1 })), // 状態を1減少
}));

上記のコードでは、set関数を使って状態を簡単に更新しています。この方法は、状態の変更が明確で理解しやすいのが特徴です。

状態の取得


Zustandでは、状態ストアをReactコンポーネント内で簡単に利用できます。useStoreフックを使って状態を取得します。

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

function DisplayCount() {
  const count = useStore((state) => state.count); // 状態を取得

  return <h1>{count}</h1>;
}

export default DisplayCount;

ここでは、状態ストアからcountを取得して表示しています。

状態の更新


状態を更新するには、ストア内で定義された関数を利用します。以下は、状態を増減するボタンを実装した例です。

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

function CounterControls() {
  const increment = useStore((state) => state.increment);
  const decrement = useStore((state) => state.decrement);

  return (
    <div>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
    </div>
  );
}

export default CounterControls;

ここでは、incrementdecrement関数を呼び出して状態を更新しています。

状態管理の構造を結合する


これらの構造を組み合わせると、以下のようなReactコンポーネントが完成します。

import React from 'react';
import DisplayCount from './DisplayCount';
import CounterControls from './CounterControls';

function App() {
  return (
    <div>
      <DisplayCount />
      <CounterControls />
    </div>
  );
}

export default App;

このように、状態ストアを複数のコンポーネントで共有しながら管理することができます。

まとめ


Zustandを使用した状態管理は、状態の定義、取得、更新の流れが非常にシンプルです。このシンプルさは、コードの可読性と保守性を高めるだけでなく、小規模プロジェクトに最適な設計を可能にします。次のセクションでは、複数コンポーネント間での状態共有についてさらに詳しく見ていきます。

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

Zustandは、Reactのコンテキストを使用せずに、複数のコンポーネント間で状態を簡単に共有できる仕組みを提供します。このセクションでは、状態を共有し、コンポーネントが連携する方法について解説します。

状態ストアを定義する


まず、状態ストアを作成します。この例では、countを共有するためのストアを定義します。

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;

ここでは、countという状態と、それを増減するための関数を定義しました。このストアは、複数のコンポーネントで共有されます。

状態を共有するコンポーネントを作成

コンポーネント1: 状態を表示する

以下のコンポーネントは、現在のcountを表示します。

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

function DisplayCount() {
  const count = useStore((state) => state.count); // 状態を取得

  return <h1>現在のカウント: {count}</h1>;
}

export default DisplayCount;

コンポーネント2: 状態を操作する

次に、状態を増減するボタンを提供するコンポーネントを作成します。

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

function CounterControls() {
  const increment = useStore((state) => state.increment);
  const decrement = useStore((state) => state.decrement);

  return (
    <div>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
    </div>
  );
}

export default CounterControls;

アプリケーションに統合


最後に、これらのコンポーネントを統合して、アプリケーション全体で状態を共有します。

import React from 'react';
import DisplayCount from './DisplayCount';
import CounterControls from './CounterControls';

function App() {
  return (
    <div>
      <DisplayCount />
      <CounterControls />
    </div>
  );
}

export default App;

状態が共有される仕組み


Zustandでは、すべてのコンポーネントが同じ状態ストアにアクセスするため、DisplayCountCounterControlscountを共有します。たとえば、CounterControlsでボタンをクリックしてcountを増減すると、その変更が即座にDisplayCountに反映されます。

利点

  1. 簡単な構造: Zustandを使用すると、Reactコンテキストを使用せずに状態共有が実現できます。
  2. 効率的なレンダリング: Zustandは、状態の更新が必要なコンポーネントだけを再レンダリングします。これにより、パフォーマンスが向上します。
  3. 明確なコード: 状態管理が明確で、複雑なコンテキストやプロバイダーの設定が不要です。

まとめ


複数のコンポーネントで状態を共有する際、ZustandはそのシンプルなAPIと効率的なレンダリングの仕組みによって、スムーズな実装を可能にします。次のセクションでは、非同期処理をZustandでどのように扱うかを見ていきます。

非同期処理への対応

Zustandは、非同期処理を簡単に統合できる柔軟性を持っています。状態ストア内に非同期関数を定義することで、サーバーからデータを取得したり、APIリクエストを処理したりする際に活用できます。このセクションでは、Zustandを使用した非同期処理の実装方法について解説します。

非同期処理を含む状態ストアの定義


Zustandでは、状態ストア内に非同期処理を簡単に追加できます。以下は、APIからデータを取得する例です。

import create from 'zustand';

// Zustandストアの定義
const useStore = create((set) => ({
  data: null, // 初期データ
  isLoading: false, // ローディング状態
  fetchData: async () => {
    set({ isLoading: true }); // ローディング状態を更新
    try {
      const response = await fetch('https://jsonplaceholder.typicode.com/posts');
      const result = await response.json();
      set({ data: result, isLoading: false }); // データを設定し、ローディングを終了
    } catch (error) {
      console.error('データ取得エラー:', error);
      set({ isLoading: false }); // ローディングを終了
    }
  },
}));

この例では、非同期関数fetchDataを定義し、データの取得中にisLoadingを更新しています。

非同期処理を利用するコンポーネント

非同期関数をコンポーネント内で呼び出し、データを表示する方法を見ていきます。

import React, { useEffect } from 'react';
import useStore from './store';

function DataDisplay() {
  const { data, isLoading, fetchData } = useStore(); // 状態と非同期関数を取得

  useEffect(() => {
    fetchData(); // コンポーネントがマウントされた際にデータを取得
  }, [fetchData]);

  if (isLoading) {
    return <p>データを読み込んでいます...</p>;
  }

  if (!data) {
    return <p>データがありません。</p>;
  }

  return (
    <div>
      <h2>取得したデータ:</h2>
      <ul>
        {data.map((item) => (
          <li key={item.id}>{item.title}</li>
        ))}
      </ul>
    </div>
  );
}

export default DataDisplay;

この例では、fetchDataがデータ取得を行い、その結果を表示します。isLoadingを利用して、データ取得中の状態をユーザーに示します。

エラー処理の追加


非同期処理ではエラー処理も重要です。Zustandのストア内でエラー状態を追加することで、エラー時のUIを表示できます。

const useStore = create((set) => ({
  data: null,
  isLoading: false,
  error: null, // エラー状態を追加
  fetchData: async () => {
    set({ isLoading: true, error: null });
    try {
      const response = await fetch('https://jsonplaceholder.typicode.com/posts');
      const result = await response.json();
      set({ data: result, isLoading: false });
    } catch (error) {
      set({ error: 'データの取得に失敗しました。', isLoading: false });
    }
  },
}));

コンポーネントでエラー状態を確認し、適切なメッセージを表示することができます。

まとめ


Zustandを使えば、非同期処理をスムーズに統合できます。非同期関数を状態ストア内で直接定義できるため、シンプルかつ効率的な実装が可能です。この手法は、APIリクエストやデータのリアルタイム更新など、さまざまなユースケースで活用できます。次のセクションでは、具体的な応用例としてZustandを利用したToDoアプリの実装を紹介します。

Zustandを利用した状態管理の応用例

ここでは、Zustandを利用してシンプルなToDoアプリを実装します。このアプリは、タスクの追加、削除、完了状態の管理を行います。Zustandの実践的な使い方を学びながら、状態管理の応用例を確認しましょう。

状態ストアの作成

ToDoアプリ用の状態ストアを作成します。

import create from 'zustand';

// Zustandストア
const useStore = create((set) => ({
  todos: [], // ToDoリスト
  addTodo: (task) =>
    set((state) => ({
      todos: [...state.todos, { id: Date.now(), task, completed: false }],
    })),
  toggleTodo: (id) =>
    set((state) => ({
      todos: state.todos.map((todo) =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      ),
    })),
  removeTodo: (id) =>
    set((state) => ({
      todos: state.todos.filter((todo) => todo.id !== id),
    })),
}));

export default useStore;

このストアでは、addTodotoggleTodoremoveTodoという3つの関数でToDoリストを操作します。

タスクの追加コンポーネント

タスクを追加する入力フォームを作成します。

import React, { useState } from 'react';
import useStore from './store';

function AddTodo() {
  const [task, setTask] = useState('');
  const addTodo = useStore((state) => state.addTodo);

  const handleSubmit = (e) => {
    e.preventDefault();
    if (task.trim()) {
      addTodo(task);
      setTask('');
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={task}
        onChange={(e) => setTask(e.target.value)}
        placeholder="タスクを入力してください"
      />
      <button type="submit">追加</button>
    </form>
  );
}

export default AddTodo;

タスクの一覧表示コンポーネント

現在のToDoリストを表示し、タスクの操作を行います。

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

function TodoList() {
  const { todos, toggleTodo, removeTodo } = useStore();

  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id}>
          <input
            type="checkbox"
            checked={todo.completed}
            onChange={() => toggleTodo(todo.id)}
          />
          <span
            style={{
              textDecoration: todo.completed ? 'line-through' : 'none',
            }}
          >
            {todo.task}
          </span>
          <button onClick={() => removeTodo(todo.id)}>削除</button>
        </li>
      ))}
    </ul>
  );
}

export default TodoList;

このコンポーネントでは、タスクの完了状態を切り替えるチェックボックスと、削除ボタンを実装しています。

アプリケーション全体

これらのコンポーネントを組み合わせて、アプリ全体を構築します。

import React from 'react';
import AddTodo from './AddTodo';
import TodoList from './TodoList';

function App() {
  return (
    <div>
      <h1>ToDoアプリ</h1>
      <AddTodo />
      <TodoList />
    </div>
  );
}

export default App;

動作確認

  1. タスクを入力して「追加」ボタンを押すと、ToDoリストにタスクが追加されます。
  2. チェックボックスをクリックすると、タスクの完了状態が切り替わります。
  3. 「削除」ボタンを押すと、タスクがリストから削除されます。

まとめ


このToDoアプリの実装例を通じて、Zustandの柔軟性と使いやすさを確認できました。Zustandを使用することで、状態管理のコード量を大幅に削減しつつ、効率的に状態を管理できます。このように、Zustandは小規模から中規模のReactプロジェクトでの実用的な選択肢として非常に有用です。次のセクションでは、状態管理における課題とその解決策について説明します。

状態管理における課題とその解決策

Zustandはシンプルで使いやすい状態管理ライブラリですが、プロジェクトによっては特定の課題に直面することがあります。このセクションでは、Zustandを使用する際に発生し得る課題とその解決方法を解説します。

課題1: 大量の状態管理が必要な場合のストア設計


Zustandは軽量でシンプルな設計が特徴ですが、大規模なプロジェクトではストアが肥大化し、管理が難しくなることがあります。

解決策

  • ストアの分割: 状態をコンポーネントごと、または機能ごとに分割して複数のストアを作成します。
  • カスタムフックの活用: 各ストアをカスタムフックとして提供し、状態を必要な範囲でのみ利用します。

例: ストアの分割

import create from 'zustand';

// UI関連の状態ストア
const useUIStore = create((set) => ({
  theme: 'light',
  toggleTheme: () => set((state) => ({ theme: state.theme === 'light' ? 'dark' : 'light' })),
}));

// データ関連の状態ストア
const useDataStore = create((set) => ({
  items: [],
  addItem: (item) => set((state) => ({ items: [...state.items, item] })),
}));

課題2: 複雑な非同期処理の管理


複数の非同期処理が絡む場合、状態の追跡やエラーハンドリングが煩雑になることがあります。

解決策

  • 状態を細分化: 各非同期処理に対応したローディング状態やエラー状態を個別に管理します。
  • 非同期処理専用のユーティリティ関数: 共通の非同期処理ロジックをユーティリティ関数として抽象化します。

例: 細分化された状態管理

const useStore = create((set) => ({
  userData: null,
  isLoading: false,
  error: null,
  fetchUserData: async (userId) => {
    set({ isLoading: true, error: null });
    try {
      const response = await fetch(`/api/users/${userId}`);
      const data = await response.json();
      set({ userData: data, isLoading: false });
    } catch (err) {
      set({ error: 'データ取得に失敗しました', isLoading: false });
    }
  },
}));

課題3: 状態更新による不要な再レンダリング


Zustandを使う際に、状態更新によって必要のないコンポーネントが再レンダリングされる場合があります。

解決策

  • セレクタを活用: useStoreで必要な状態だけを選択し、余計なレンダリングを防ぎます。
  • シャロウ比較の導入: Zustandのセレクタにシャロウ比較を適用して、状態の変更がない場合に再レンダリングを抑制します。

例: セレクタとシャロウ比較の使用

import shallow from 'zustand/shallow';

const Component = () => {
  const { count, increment } = useStore(
    (state) => ({ count: state.count, increment: state.increment }),
    shallow
  );

  return (
    <div>
      <h1>{count}</h1>
      <button onClick={increment}>増加</button>
    </div>
  );
};

課題4: 状態の初期化やリセット


テストやページ遷移の際に、状態をリセットする必要がある場合があります。

解決策

  • 初期状態を定義: 初期状態を一元管理し、リセット関数を作成します。

例: 状態のリセット機能

const useStore = create((set) => {
  const initialState = { count: 0 };

  return {
    ...initialState,
    reset: () => set(initialState),
  };
});

まとめ


Zustandはシンプルで強力な状態管理ライブラリですが、プロジェクトの規模や要求によっては課題が生じることがあります。これらの課題に対しては、ストアの分割、セレクタやユーティリティの活用、状態リセットの仕組みなどを適切に組み合わせることで対応できます。次のセクションでは、これまでの内容をまとめます。

まとめ

本記事では、Reactでの状態管理におけるZustandの使い方と、その応用例について詳しく解説しました。Zustandの基本的な使用法から始まり、非同期処理や複数コンポーネントでの状態共有、そしてToDoアプリの実装例までを通じて、Zustandのシンプルさと柔軟性を実感していただけたと思います。

さらに、状態管理における課題とその解決策についても触れ、実践で役立つ方法を紹介しました。Zustandを使用することで、状態管理を効率化し、React開発をよりスムーズに進めることが可能です。

今後のプロジェクトで、軽量かつ直感的な状態管理が必要な場合には、ぜひZustandを活用してみてください。状態管理がより簡単になり、開発のスピードと効率が向上することでしょう。

コメント

コメントする

目次