ZustandとReact Queryで実現する統合的な状態とデータ管理の実例

Reactアプリケーションを開発する際、状態管理とデータフェッチの管理は欠かせない課題です。従来の状態管理ライブラリやデータ管理ツールは、それぞれの分野で強力ですが、分離して使うとコードが複雑化しがちです。そこで、軽量な状態管理ライブラリであるZustandと、データ取得やキャッシュ管理に優れたReact Queryを組み合わせることで、効率的で統合的な管理を実現する方法があります。本記事では、両者の基本的な特徴から具体的な実装例、さらに大規模アプリケーションでの応用までを解説し、現代のReact開発における強力なツールチェーンを提案します。

目次
  1. Zustandとは何か
    1. Zustandの主な特徴
    2. コード例
    3. Zustandの利点
  2. React Queryの役割
    1. React Queryの主な特徴
    2. コード例
    3. React Queryの利点
  3. ZustandとReact Queryの違い
    1. Zustandの役割
    2. React Queryの役割
    3. ZustandとReact Queryの補完関係
    4. 使い分けの指針
  4. 組み合わせによるメリット
    1. 1. 状態管理とデータ管理の明確な分離
    2. 2. 再利用可能な状態管理
    3. 3. 柔軟なデータ更新
    4. 4. パフォーマンスの最適化
    5. 5. デバッグと保守の簡易化
  5. 実装例: シンプルなToDoアプリ
    1. 1. 必要な依存関係のインストール
    2. 2. Zustandでフィルタリング状態を管理
    3. 3. React Queryでデータを管理
    4. 4. ToDoアプリの実装
    5. 5. コードのポイント
    6. 6. 実装の結果
  6. 状態管理のパターン
    1. 1. グローバル状態とサーバーデータの役割分担
    2. 2. クエリとローカル状態の分離
    3. 3. ロジックの分離と再利用性
    4. 4. 状態の永続化
    5. 5. コンポーネントの最小化
    6. 6. 統一されたパターンの利点
  7. 状態とデータのテスト戦略
    1. 1. Zustandの状態管理テスト
    2. 2. React Queryのデータ管理テスト
    3. 3. ZustandとReact Queryの統合テスト
    4. 4. テスト戦略のポイント
    5. 5. テスト戦略の利点
  8. トラブルシューティング
    1. 1. Zustandの状態が更新されない
    2. 2. React Queryのキャッシュが期待通りに更新されない
    3. 3. ZustandとReact Queryの連携がうまくいかない
    4. 4. パフォーマンスの問題
    5. 5. データの同期が取れない
    6. 6. エラー処理が不十分
    7. 7. デバッグの難しさ
    8. 8. トラブルシューティングのまとめ
  9. 応用例: 大規模アプリケーションへの展開
    1. 1. 状態とデータのモジュール化
    2. 2. 状態とデータの同期
    3. 3. 複雑な状態管理の解決
    4. 4. エラー処理とロギング
    5. 5. 分散チームでの開発
    6. 6. 利点のまとめ
  10. まとめ

Zustandとは何か


Zustandは、React向けの軽量で柔軟な状態管理ライブラリです。ReduxやContext APIのような他の状態管理ツールと比較して、シンプルなAPIと効率性が特徴です。Zustandでは、状態を簡潔に定義でき、非同期データやネストされた状態も容易に扱えます。

Zustandの主な特徴

  • シンプルなAPI: 状態を定義し、利用するまでの流れが直感的です。
  • 軽量で効率的: 必要な部分のみを再レンダリングするため、パフォーマンスに優れています。
  • 非React依存: ZustandはReactに依存しないため、他の環境でも使用できます。

コード例


以下は、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 })),
}));

function Counter() {
  const { count, increment, decrement } = useStore();
  return (
    <div>
      <h1>{count}</h1>
      <button onClick={increment}>Increment</button>
      <button onClick={decrement}>Decrement</button>
    </div>
  );
}

Zustandの利点

  • 直感的な状態管理: シンプルなコードで状態を操作可能。
  • スケーラブル: アプリが成長しても管理が容易。
  • 容易な統合: 他のツールやライブラリと簡単に連携できる。

Zustandは、状態管理をシンプルかつ効率的に行いたい場合に最適な選択肢となります。次に、React Queryがどのようにデータ管理を支えるのかを解説します。

React Queryの役割


React Queryは、Reactアプリケーションでのデータフェッチやキャッシング、状態管理を効率化するためのライブラリです。特に非同期データの取得や更新が多いアプリケーションにおいて、APIとの通信を簡素化し、データの整合性を保つことに優れています。

React Queryの主な特徴

  • 非同期データの管理: APIからのデータ取得やキャッシュの更新を簡潔に実装可能。
  • 再利用性の高いロジック: データフェッチのロジックをカスタムフックとして再利用できる。
  • 自動リフレッシュ: データが古くなると自動で更新し、最新状態を保つ。
  • エラー処理とリトライ機能: ネットワークエラーなどに対する高度なリトライオプションを備える。

コード例


以下は、React Queryを用いたデータ取得の基本例です:

import { useQuery } from '@tanstack/react-query';

function fetchUserData() {
  return fetch('https://api.example.com/user').then((res) => res.json());
}

function UserComponent() {
  const { data, error, isLoading } = useQuery(['user'], fetchUserData);

  if (isLoading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;

  return (
    <div>
      <h1>{data.name}</h1>
      <p>{data.email}</p>
    </div>
  );
}

React Queryの利点

  • 宣言的なデータフェッチ: 状態や副作用を手動で管理する必要がない。
  • キャッシュの活用: データをキャッシュに保存し、不要なリクエストを削減。
  • 自動リフレッシュと並列処理: 他のリソースと連携しつつデータを同期可能。

React Queryは、非同期データを管理しながらパフォーマンスと開発効率を向上させる強力なツールです。次は、ZustandとReact Queryをどのように使い分け、補完し合うのかを解説します。

ZustandとReact Queryの違い


ZustandとReact Queryは、それぞれ異なる用途に特化したライブラリですが、組み合わせて使用することで相互補完的な関係を構築できます。本セクションでは、両者の役割と使い分けについて詳しく解説します。

Zustandの役割


Zustandは、アプリケーション内のローカルな状態を管理するために使用されます。以下のようなケースで特に役立ちます:

  • UIの状態: ダイアログの開閉、フォームの入力値など。
  • アプリケーション固有の設定: テーマや言語の選択。
  • 一時的なデータ: リクエストの進行状況や入力データのキャッシュ。

使用例: UI状態の管理

const useUiStore = create((set) => ({
  isModalOpen: false,
  toggleModal: () => set((state) => ({ isModalOpen: !state.isModalOpen })),
}));

// コンポーネントでの利用
function Modal() {
  const { isModalOpen, toggleModal } = useUiStore();
  return (
    <>
      <button onClick={toggleModal}>Toggle Modal</button>
      {isModalOpen && <div>Modal Content</div>}
    </>
  );
}

React Queryの役割


React Queryは、外部ソースからのデータ取得やキャッシュ管理に特化しています。特に以下のようなケースで有用です:

  • サーバーからのデータ取得: REST APIやGraphQLからのデータフェッチ。
  • キャッシュ管理: サーバーサイドデータのキャッシュと自動更新。
  • エラー処理: ネットワーク障害時のリトライ機能。

使用例: サーバーデータの管理

const { data, error, isLoading } = useQuery(['todos'], fetchTodos);

ZustandとReact Queryの補完関係


ZustandとReact Queryは、それぞれ異なる種類のデータを管理しますが、両者を組み合わせることで以下のようなメリットがあります:

  • UI状態とサーバーデータの分離: React Queryは非同期データを管理し、ZustandはUI状態を制御する。
  • 簡潔なコード: 状態管理とデータフェッチのロジックを分離し、コードをシンプルに保てる。
  • 柔軟な統合: React QueryのキャッシュデータをZustandの状態に反映させるなど、両者を連携可能。

使い分けの指針

  • ローカルで完結する状態: Zustandを使用。
  • サーバーから取得するデータ: React Queryを使用。
  • 状態の中に非同期データを組み込む必要がある場合: 両者を統合して使用。

次は、この2つを組み合わせることで得られるメリットについて、具体的なシナリオをもとに解説します。

組み合わせによるメリット


ZustandとReact Queryを組み合わせることで、それぞれの強みを活かしながら効率的な状態管理とデータ管理を実現できます。以下では、この組み合わせがもたらす具体的な利点をシナリオ別に解説します。

1. 状態管理とデータ管理の明確な分離


React Queryがサーバーデータの管理に特化している一方で、Zustandはローカルの状態管理に特化しています。この分離により、コードの可読性が向上し、データの扱い方が明確になります。

  • : ダッシュボードで、React QueryがAPIから取得したデータを管理し、Zustandがウィジェットのレイアウトやユーザーインタラクションの状態を管理する。

2. 再利用可能な状態管理


Zustandで状態を管理することで、サーバーデータに依存しない状態を再利用可能な形で分離できます。これにより、複数のコンポーネントで一貫したローカル状態を共有できます。

  • : フィルターやソート条件をZustandで管理し、React Queryのクエリ関数に渡してサーバーデータを取得。

3. 柔軟なデータ更新


React Queryのmutate関数を使用してデータを更新し、その結果をZustandのローカル状態に反映させることで、状態の一貫性を保ちながら効率的に更新できます。

  • : ユーザーの設定を変更する際、React QueryでAPIリクエストを行い、成功時にZustandでUI状態を更新する。

コード例: 組み合わせの実装


以下は、ZustandとReact Queryを組み合わせた例です:

import create from 'zustand';
import { useQuery, useMutation } from '@tanstack/react-query';

// Zustand: ローカル状態管理
const useStore = create((set) => ({
  filter: 'all',
  setFilter: (filter) => set({ filter }),
}));

// React Query: サーバーデータ管理
function fetchTodos(filter) {
  return fetch(`/api/todos?filter=${filter}`).then((res) => res.json());
}

export function TodoApp() {
  const { filter, setFilter } = useStore();
  const { data: todos, isLoading } = useQuery(['todos', filter], () => fetchTodos(filter));
  const mutation = useMutation((newTodo) => fetch('/api/todos', {
    method: 'POST',
    body: JSON.stringify(newTodo),
  }));

  if (isLoading) return <p>Loading...</p>;

  return (
    <div>
      <select onChange={(e) => setFilter(e.target.value)} value={filter}>
        <option value="all">All</option>
        <option value="completed">Completed</option>
        <option value="pending">Pending</option>
      </select>
      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>{todo.title}</li>
        ))}
      </ul>
      <button
        onClick={() => mutation.mutate({ title: 'New Todo' })}
      >
        Add Todo
      </button>
    </div>
  );
}

4. パフォーマンスの最適化


React Queryはキャッシュ機能やデータのプリフェッチを提供し、Zustandは必要な部分のみ再レンダリングします。この組み合わせにより、リソース効率の良いアプリケーションを構築できます。

5. デバッグと保守の簡易化


Zustandの直感的なAPIとReact Queryの強力なDevToolsを活用することで、状態とデータの問題を簡単に特定して解決できます。

次は、この2つの組み合わせを具体的に示した実装例として、シンプルなToDoアプリを紹介します。

実装例: シンプルなToDoアプリ


ZustandとReact Queryを組み合わせたToDoアプリの実装を通して、統合的な状態管理とデータ管理を具体的に紹介します。この例では、Zustandでフィルタリング状態を管理し、React Queryでサーバーデータを取得および更新します。

1. 必要な依存関係のインストール

npm install @tanstack/react-query zustand

2. Zustandでフィルタリング状態を管理


Zustandを利用して、現在のフィルタ状態を管理します:

import create from 'zustand';

const useFilterStore = create((set) => ({
  filter: 'all',
  setFilter: (filter) => set({ filter }),
}));

export default useFilterStore;

3. React Queryでデータを管理


React Queryを使ってToDoリストを取得および更新します:

import { useQuery, useMutation, QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient();

function fetchTodos(filter) {
  return fetch(`/api/todos?filter=${filter}`).then((res) => res.json());
}

function addTodo(newTodo) {
  return fetch('/api/todos', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(newTodo),
  }).then((res) => res.json());
}

4. ToDoアプリの実装


以下に、ZustandとReact Queryを統合したアプリのコードを示します:

import React from 'react';
import useFilterStore from './useFilterStore';
import { useQuery, useMutation, QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient();

function TodoApp() {
  const { filter, setFilter } = useFilterStore();
  const { data: todos = [], isLoading } = useQuery(['todos', filter], () => fetchTodos(filter));
  const mutation = useMutation(addTodo, {
    onSuccess: () => queryClient.invalidateQueries(['todos']),
  });

  if (isLoading) return <p>Loading...</p>;

  return (
    <div>
      <h1>Todo App</h1>
      <select onChange={(e) => setFilter(e.target.value)} value={filter}>
        <option value="all">All</option>
        <option value="completed">Completed</option>
        <option value="pending">Pending</option>
      </select>
      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>{todo.title}</li>
        ))}
      </ul>
      <button
        onClick={() => mutation.mutate({ title: 'New Todo', status: 'pending' })}
      >
        Add Todo
      </button>
    </div>
  );
}

export default function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <TodoApp />
    </QueryClientProvider>
  );
}

5. コードのポイント

  1. Zustandの役割: filter状態を管理し、ユーザーの選択に基づいてデータのフィルタリングを行う。
  2. React Queryの役割: サーバーからデータを取得し、ToDoアイテムの追加や変更を処理する。
  3. 統合の方法: React QueryのクエリキーにZustandの状態を含め、フィルタリング状態をリアルタイムで反映させる。

6. 実装の結果


このアプリは、ZustandによるUI状態管理とReact Queryによるデータ管理を統合しており、ユーザーの操作に応じた効率的な状態更新とデータ取得が可能です。

次は、この状態管理のパターンについてさらに詳しく説明します。

状態管理のパターン


ZustandとReact Queryを組み合わせることで、状態とデータの管理を効率化できますが、そのためには適切なパターンを構築することが重要です。本セクションでは、統一された状態管理パターンを導入し、スケーラブルなアプリケーションを構築する方法を解説します。

1. グローバル状態とサーバーデータの役割分担

  • Zustandで管理するべき状態
  • UIに関連する一時的な状態(例: モーダルの開閉、フォームの入力値)。
  • サーバーデータに直接関係しないローカルの設定(例: ユーザーのテーマ選択)。
  • React Queryで管理するべきデータ
  • サーバーから取得したデータ(例: ユーザー情報、商品リスト)。
  • データのキャッシュと更新(例: CRUD操作の結果)。

2. クエリとローカル状態の分離


React QueryとZustandを統合する際、役割を明確に分離することでコードが整理されます。

  • クエリキーにローカル状態を活用して、状態変更に基づくデータ取得を効率化します。
  • ローカル状態の変更が即座にデータフェッチに反映される構造を構築します。

const useFilterStore = create((set) => ({
  filter: 'all',
  setFilter: (filter) => set({ filter }),
}));

function TodoList() {
  const { filter } = useFilterStore();
  const { data: todos } = useQuery(['todos', filter], () => fetchTodos(filter));

  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id}>{todo.title}</li>
      ))}
    </ul>
  );
}

3. ロジックの分離と再利用性


状態管理とデータ管理をカスタムフックに抽出することで、再利用性を高め、コンポーネントをシンプルに保つことができます。

カスタムフック例

function useTodos() {
  const { filter } = useFilterStore();
  const { data, isLoading, error } = useQuery(['todos', filter], () => fetchTodos(filter));
  return { todos: data, isLoading, error };
}

コンポーネントでの利用

function TodoList() {
  const { todos, isLoading } = useTodos();

  if (isLoading) return <p>Loading...</p>;

  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id}>{todo.title}</li>
      ))}
    </ul>
  );
}

4. 状態の永続化


Zustandは、ミドルウェアを活用することで状態を永続化できます。ローカルストレージやセッションストレージを使うことで、アプリケーションの再起動時にも状態を保持できます。

永続化ミドルウェアの利用例

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

const usePersistentStore = create(
  persist(
    (set) => ({
      theme: 'light',
      setTheme: (theme) => set({ theme }),
    }),
    { name: 'app-settings' } // ローカルストレージキー
  )
);

5. コンポーネントの最小化


ZustandとReact Queryの組み合わせにより、コンポーネントが状態管理やデータフェッチの詳細を直接扱わなくても済むため、責務が明確になります。これにより、アプリケーションのメンテナンス性が向上します。

6. 統一されたパターンの利点

  • 可読性: 状態とデータのロジックが明確に分離されるため、コードが理解しやすい。
  • 再利用性: カスタムフックによりロジックを共通化し、複数のコンポーネントで使用可能。
  • スケーラビリティ: 状態とデータが統一されたパターンで管理されるため、規模が拡大しても対応可能。

次は、この状態管理とデータ管理のテスト戦略について解説します。

状態とデータのテスト戦略


ZustandとReact Queryを使用したアプリケーションでは、状態管理とデータ管理のテストを効率的に行うために適切な戦略を採用することが重要です。このセクションでは、ローカル状態とサーバーデータのテスト方法を具体的に解説します。

1. Zustandの状態管理テスト


Zustandの状態は純粋な関数として定義されているため、直接的にテスト可能です。以下のポイントを押さえてテストを行います:

  • 状態の初期値を確認する。
  • アクションが期待通りに状態を変更するか確認する。

例: Zustandのテスト

import create from 'zustand';

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

test('Zustand state updates correctly', () => {
  const store = useTestStore.getState();
  expect(store.count).toBe(0); // 初期値の確認

  store.increment();
  expect(useTestStore.getState().count).toBe(1); // 増加後の確認

  store.decrement();
  expect(useTestStore.getState().count).toBe(0); // 減少後の確認
});

2. React Queryのデータ管理テスト


React Queryは非同期データを扱うため、テストにはモックAPIを使用するのが一般的です。以下をテストします:

  • クエリが適切にデータを取得するか。
  • データ取得時やエラー時の状態が正しく設定されるか。

例: React Queryのテスト

import { renderHook } from '@testing-library/react-hooks';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query';

const queryClient = new QueryClient();

function useMockData() {
  return useQuery(['mock'], async () => {
    return { data: 'Mocked data' };
  });
}

test('React Query fetches data correctly', async () => {
  const wrapper = ({ children }) => (
    <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
  );
  const { result, waitFor } = renderHook(() => useMockData(), { wrapper });

  await waitFor(() => result.current.isSuccess);

  expect(result.current.data).toEqual({ data: 'Mocked data' }); // データの確認
});

3. ZustandとReact Queryの統合テスト


両者を統合した場合、状態管理とデータ管理が正しく連携するかをテストする必要があります。特に、状態がデータフェッチに反映されるかを確認します。

例: 統合テスト

import { render, screen, fireEvent } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { TodoApp } from './TodoApp'; // ToDoアプリの例

test('TodoApp renders and updates correctly', async () => {
  const queryClient = new QueryClient();
  render(
    <QueryClientProvider client={queryClient}>
      <TodoApp />
    </QueryClientProvider>
  );

  // 初期データの確認
  expect(await screen.findByText('Loading...')).toBeInTheDocument();

  // フィルタの更新と再レンダリングの確認
  fireEvent.change(screen.getByRole('combobox'), { target: { value: 'completed' } });
  expect(await screen.findByText('Completed Todo Item')).toBeInTheDocument();
});

4. テスト戦略のポイント

  • 分離テスト: ZustandとReact Queryのテストは分離して行い、各ライブラリの挙動を個別に確認する。
  • 統合テスト: 状態とデータが連携する機能をE2E(エンドツーエンド)テストで確認する。
  • モックデータの活用: React Queryのテストでは、モックAPIを使用して外部依存を排除する。

5. テスト戦略の利点

  • バグの早期発見: 状態とデータの連携不具合を早期に検出できる。
  • 安定性の向上: 複雑な状態とデータの管理ロジックの信頼性が向上する。
  • 保守性の確保: リファクタリング後も動作を保証できる。

次は、トラブルシューティングについて解説し、よくある問題とその解決方法を紹介します。

トラブルシューティング


ZustandとReact Queryを組み合わせた状態管理は強力ですが、実際の開発では予期しない問題が発生することがあります。このセクションでは、よくある問題とその解決方法について解説します。

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


原因: Zustandの状態更新関数が正しく呼び出されていない場合や、状態がコンポーネントで利用されていない場合に発生します。

解決方法:

  • Zustandのset関数が正しく実装されているか確認します。
  • 状態を利用しているコンポーネントが、状態の依存関係を正しく設定しているか確認します。

:

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

function Counter() {
  const { count, increment } = useStore(); // 必要な状態を明示的に取得
  return (
    <div>
      <p>{count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
}

2. React Queryのキャッシュが期待通りに更新されない


原因: キャッシュの無効化(invalidateQueries)が正しく呼び出されていない場合や、クエリキーが適切でない場合に発生します。

解決方法:

  • データ更新後にqueryClient.invalidateQueriesを適切に使用する。
  • クエリキーが一貫しているか確認する。

:

import { useMutation, useQueryClient } from '@tanstack/react-query';

const mutation = useMutation(addTodo, {
  onSuccess: () => {
    queryClient.invalidateQueries(['todos']); // キャッシュを無効化
  },
});

3. ZustandとReact Queryの連携がうまくいかない


原因: Zustandの状態変更がReact Queryのクエリに反映されない場合に発生します。

解決方法:

  • Zustandの状態をReact Queryのクエリキーに正しく関連付ける。
  • Zustandの状態変更がトリガーされるタイミングでReact Queryの再フェッチが発生するようにする。

:

const { filter, setFilter } = useFilterStore();
const { data } = useQuery(['todos', filter], () => fetchTodos(filter));

4. パフォーマンスの問題


原因: Zustandの状態が頻繁に更新される、React Queryで不要なフェッチが発生するなど。

解決方法:

  • Zustandの状態を必要最小限に設計する。
  • React Queryのフェッチ頻度をstaleTimecacheTimeで調整する。

:

const { data } = useQuery(['todos'], fetchTodos, {
  staleTime: 1000 * 60, // 1分間データを新鮮とみなす
});

5. データの同期が取れない


原因: Zustandの状態とReact Queryのキャッシュが一致しない場合に発生します。

解決方法:

  • データの同期が必要な場合、React QueryのonSuccessonSettledでZustandの状態を更新する。

:

const mutation = useMutation(addTodo, {
  onSuccess: (data) => {
    useStore.setState({ todos: [...useStore.getState().todos, data] }); // Zustandに反映
  },
});

6. エラー処理が不十分


原因: APIエラーや状態管理エラーが考慮されていない。

解決方法:

  • React QueryのonErrorでエラー処理を実装する。
  • Zustandの状態でエラーメッセージを管理する場合、専用のエラーストアを作成する。

:

const useErrorStore = create((set) => ({
  errorMessage: '',
  setErrorMessage: (message) => set({ errorMessage: message }),
}));

const mutation = useMutation(addTodo, {
  onError: (error) => {
    useErrorStore.setState({ errorMessage: error.message });
  },
});

7. デバッグの難しさ


原因: 状態とデータの変更が多いアプリケーションでは、変更の追跡が困難になることがあります。

解決方法:

  • Zustandのミドルウェアでロギングを有効にする。
  • React Query DevToolsを利用してデータの状態を確認する。

:

import { devtools } from 'zustand/middleware';

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

8. トラブルシューティングのまとめ

  • Zustand: 状態が正しく更新されるよう、set関数の実装と依存関係の設定を確認する。
  • React Query: クエリキーとキャッシュの無効化処理を見直し、適切な設定を行う。
  • 統合: 状態とデータの同期を意識し、テストで動作を確認する。

次は、ZustandとReact Queryを用いた大規模アプリケーションでの応用例について解説します。

応用例: 大規模アプリケーションへの展開


ZustandとReact Queryの組み合わせは、シンプルなアプリケーションだけでなく、大規模アプリケーションでも効果を発揮します。本セクションでは、大規模プロジェクトでの応用例を紹介し、拡張性や効率性をどのように実現するかを解説します。

1. 状態とデータのモジュール化


大規模アプリケーションでは、状態やデータ管理の分散化が必要です。ZustandとReact Queryを機能ごとにモジュール化することで、可読性とメンテナンス性が向上します。

例: Zustandのモジュール化

// userStore.js
import create from 'zustand';

export const useUserStore = create((set) => ({
  user: null,
  setUser: (user) => set({ user }),
}));

例: React Queryのモジュール化

// userApi.js
import { useQuery, useMutation } from '@tanstack/react-query';

export function useUserData() {
  return useQuery(['user'], () => fetch('/api/user').then((res) => res.json()));
}

export function useUpdateUser() {
  return useMutation((user) => fetch('/api/user', {
    method: 'PUT',
    body: JSON.stringify(user),
  }));
}

2. 状態とデータの同期


ユーザーが操作したローカル状態を即座にサーバーに反映し、他のユーザーとも同期するリアルタイムアプリケーションを構築できます。

例: ZustandとReact Queryのリアルタイム同期

import { useUserStore } from './userStore';
import { useUpdateUser } from './userApi';

export function UserProfile() {
  const { user, setUser } = useUserStore();
  const mutation = useUpdateUser();

  const updateUser = (newData) => {
    setUser(newData);
    mutation.mutate(newData); // サーバーに反映
  };

  return (
    <div>
      <h1>{user?.name}</h1>
      <button onClick={() => updateUser({ name: 'Updated Name' })}>
        Update Name
      </button>
    </div>
  );
}

3. 複雑な状態管理の解決


大規模なUIコンポーネントで多くの状態を扱う場合、Zustandのスライス(状態の分割)を利用すると効果的です。

例: Zustandのスライス管理

import create from 'zustand';

const useStore = create((set) => ({
  userSlice: {
    user: null,
    setUser: (user) => set((state) => ({ userSlice: { ...state.userSlice, user } })),
  },
  settingsSlice: {
    theme: 'light',
    setTheme: (theme) => set((state) => ({ settingsSlice: { ...state.settingsSlice, theme } })),
  },
}));

export const useUserSlice = () => useStore((state) => state.userSlice);
export const useSettingsSlice = () => useStore((state) => state.settingsSlice);

4. エラー処理とロギング


大規模アプリケーションでは、エラー処理とデバッグが重要です。ZustandとReact QueryのミドルウェアやDevToolsを活用すると効果的です。

React Queryのエラー処理

const { mutate } = useMutation(updateUser, {
  onError: (error) => {
    console.error('Error updating user:', error);
  },
  onSuccess: () => {
    console.log('User updated successfully');
  },
});

5. 分散チームでの開発


ZustandとReact Queryの組み合わせは、状態とデータを分離できるため、チーム間での役割分担が容易になります。たとえば、バックエンドチームはReact Queryのデータ管理ロジックを構築し、フロントエンドチームはZustandを利用してUIロジックを実装します。

6. 利点のまとめ

  • スケーラビリティ: 機能単位でモジュール化できるため、大規模なプロジェクトでも対応可能。
  • リアルタイム性: ローカル状態とサーバーデータの同期が容易。
  • 分業の効率化: 状態管理とデータ管理を分離し、開発の効率を向上。

次は、これらの技術の可能性を振り返り、統合管理の重要性についてまとめます。

まとめ


本記事では、ReactアプリケーションにおいてZustandとReact Queryを組み合わせて状態とデータを統合管理する方法を解説しました。それぞれのライブラリの特性を活かしながら、ローカル状態とサーバーデータを明確に分離し、効率的な開発を実現するアプローチを紹介しました。

具体的には、以下のポイントを取り上げました:

  • Zustandのシンプルで柔軟なローカル状態管理の特性。
  • React Queryによる非同期データフェッチとキャッシュ管理の強力な機能。
  • 両者を統合することで、コードの可読性とメンテナンス性を向上。
  • 大規模アプリケーションにおけるモジュール化やスケーラブルな設計。

ZustandとReact Queryの組み合わせは、小規模なアプリケーションから大規模なプロジェクトまで幅広く対応できる柔軟性を提供します。これらのツールを活用することで、React開発における状態管理とデータ管理の課題を効果的に解決できるでしょう。

コメント

コメントする

目次
  1. Zustandとは何か
    1. Zustandの主な特徴
    2. コード例
    3. Zustandの利点
  2. React Queryの役割
    1. React Queryの主な特徴
    2. コード例
    3. React Queryの利点
  3. ZustandとReact Queryの違い
    1. Zustandの役割
    2. React Queryの役割
    3. ZustandとReact Queryの補完関係
    4. 使い分けの指針
  4. 組み合わせによるメリット
    1. 1. 状態管理とデータ管理の明確な分離
    2. 2. 再利用可能な状態管理
    3. 3. 柔軟なデータ更新
    4. 4. パフォーマンスの最適化
    5. 5. デバッグと保守の簡易化
  5. 実装例: シンプルなToDoアプリ
    1. 1. 必要な依存関係のインストール
    2. 2. Zustandでフィルタリング状態を管理
    3. 3. React Queryでデータを管理
    4. 4. ToDoアプリの実装
    5. 5. コードのポイント
    6. 6. 実装の結果
  6. 状態管理のパターン
    1. 1. グローバル状態とサーバーデータの役割分担
    2. 2. クエリとローカル状態の分離
    3. 3. ロジックの分離と再利用性
    4. 4. 状態の永続化
    5. 5. コンポーネントの最小化
    6. 6. 統一されたパターンの利点
  7. 状態とデータのテスト戦略
    1. 1. Zustandの状態管理テスト
    2. 2. React Queryのデータ管理テスト
    3. 3. ZustandとReact Queryの統合テスト
    4. 4. テスト戦略のポイント
    5. 5. テスト戦略の利点
  8. トラブルシューティング
    1. 1. Zustandの状態が更新されない
    2. 2. React Queryのキャッシュが期待通りに更新されない
    3. 3. ZustandとReact Queryの連携がうまくいかない
    4. 4. パフォーマンスの問題
    5. 5. データの同期が取れない
    6. 6. エラー処理が不十分
    7. 7. デバッグの難しさ
    8. 8. トラブルシューティングのまとめ
  9. 応用例: 大規模アプリケーションへの展開
    1. 1. 状態とデータのモジュール化
    2. 2. 状態とデータの同期
    3. 3. 複雑な状態管理の解決
    4. 4. エラー処理とロギング
    5. 5. 分散チームでの開発
    6. 6. 利点のまとめ
  10. まとめ