【Zustand】Reactの状態管理を簡単・軽量に!

React React

「また新しいReactのライブラリ?覚えることが多すぎて疲れちゃう...」

そんな声が聞こえてきそうですね。でも、ちょっと待ってください!Zustandは、あなたのReact開発を劇的に簡単にする可能性を秘めています。

状態管理って、面倒くさいと感じていませんか?Reduxの複雑さに辟易していませんか?そんなあなたに、Zustandという新しい選択肢をご紹介します。

この記事では、Zustandの使い方をわかりやすく解説し、あなたのReactアプリケーション開発をより効率的で楽しいものにする方法をお伝えします。

難しそうに聞こえるかもしれませんが、心配無用です。この記事を読めば、あなたもZustandマスターへの第一歩を踏み出せるはず。さあ、一緒にZustandの世界を探検しましょう!

はじめに:Zustandとは?

Zustandは、React用の軽量で高速な状態管理ライブラリです。その名前は、ドイツ語で「状態」を意味する "Zustand" に由来しています。まるでドイツ車のように、シンプルでありながら高性能な設計が特徴です。

Zustandの魅力は、その使いやすさにあります。Reduxのような複雑なセットアップは必要ありません。ボイラープレートコードも最小限で済みます。さらに、TypeScriptとの相性も抜群です。

Zustandの主な特徴

  1. シンプルなAPI: 学習曲線が緩やかで、初心者でも扱いやすい
  2. 軽量: バンドルサイズが小さく、パフォーマンスへの影響も最小限
  3. 柔軟性: 様々な使用シーンに対応可能
  4. DevTools対応: 開発時のデバッグが容易
  5. TypeScript親和性: 型安全な開発をサポート

Zustandのインストール方法

まずは、Zustandをプロジェクトにインストールしましょう。npmを使用する場合は、以下のコマンドを実行します。

npm install zustand

yarnを使用する場合は、こちらのコマンドです。

yarn add zustand

これだけで、Zustandを使う準備が整いました。シンプルでしょう?

Zustandの基本的な使い方

Zustandの基本的な使い方を、ステップバイステップで見ていきましょう。

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

Zustandでは、create関数を使ってストアを作成します。以下は、カウンターアプリのための簡単なストアの例です。

import create from 'zustand'

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

この例では、countという状態と、それを操作するincrementdecrementという関数を定義しています。

ステップ2: コンポーネントでのストアの使用

作成したストアは、Reactコンポーネント内で簡単に使用できます。

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

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

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
    </div>
  )
}

これだけで、状態管理が実現できます。ReduxのようなProviderのラップや、複雑なセットアップは必要ありません。

Zustandの高度な使い方

基本的な使い方を理解したところで、Zustandのより高度な機能を見ていきましょう。

非同期アクション

Zustandでは、非同期アクションの実装も簡単です。以下は、APIからデータを取得する例です。

import create from 'zustand'

const useStore = create((set) => ({
  data: null,
  loading: false,
  error: null,
  fetchData: async () => {
    set({ loading: true })
    try {
      const response = await fetch('https://api.example.com/data')
      const data = await response.json()
      set({ data, loading: false })
    } catch (error) {
      set({ error, loading: false })
    }
  }
}))

このストアを使用するコンポーネントは以下のようになります:

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

function DataComponent() {
  const { data, loading, error, fetchData } = useStore()

  useEffect(() => {
    fetchData()
  }, [])

  if (loading) return <div>Loading...</div>
  if (error) return <div>Error: {error.message}</div>
  if (!data) return null

  return <div>{JSON.stringify(data)}</div>
}

ストアの分割

大規模なアプリケーションでは、ストアを複数の小さなストアに分割することが有効です。Zustandでは、これを簡単に実現できます。

import create from 'zustand'

const createUserSlice = (set) => ({
  user: null,
  setUser: (user) => set({ user }),
})

const createPostSlice = (set) => ({
  posts: [],
  addPost: (post) => set((state) => ({ posts: [...state.posts, post] })),
})

const useStore = create((set) => ({
  ...createUserSlice(set),
  ...createPostSlice(set),
}))

この方法で、関心事ごとにストアを分割し、メンテナンス性を向上させることができます。

ミドルウェアの使用

Zustandは、ミドルウェアをサポートしています。例えば、localStorageとの同期を行うミドルウェアを実装してみましょう。

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

const useStore = create(
  persist(
    (set) => ({
      count: 0,
      increment: () => set((state) => ({ count: state.count + 1 })),
    }),
    {
      name: 'count-storage',
      getStorage: () => localStorage,
    }
  )
)

このように、persistミドルウェアを使用することで、状態をlocalStorageに自動的に保存し、ページのリロード後も状態を維持することができます。

Zustandとの快適な開発ライフ

Zustandを使いこなすことで、Reactアプリケーションの開発がより快適になります。以下に、Zustandを使う上でのベストプラクティスをいくつか紹介します。

適切な粒度でのストア設計

ストアは、アプリケーションの規模や要件に応じて適切な粒度で設計しましょう。小規模なアプリケーションであれば、単一のストアで十分かもしれません。一方、大規模なアプリケーションでは、機能ごとに複数のストアを作成し、それぞれを独立して管理することをお勧めします。

セレクタの活用

Zustandのセレクタ機能を活用することで、パフォーマンスを最適化できます。セレクタを使用すると、ストアの特定の部分のみを購読でき、不要な再レンダリングを防ぐことができます。

const useUsername = useStore((state) => state.user.name)

DevToolsの活用

Zustandは、Redux DevToolsとの連携をサポートしています。これを活用することで、状態の変化を視覚的に追跡し、デバッグを容易に行うことができます。

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

const useStore = create(devtools((set) => ({
  // ストアの定義
})))

TypeScriptとの併用

TypeScriptを使用している場合、Zustandと組み合わせることで型安全な開発が可能になります。ストアの型定義を明示的に行うことで、開発時のエラーを事前に防ぐことができます。

import create from 'zustand'

interface BearState {
  bears: number
  increase: (by: number) => void
}

const useStore = create<BearState>((set) => ({
  bears: 0,
  increase: (by) => set((state) => ({ bears: state.bears + by })),
}))

テストの簡素化

Zustandを使用すると、ユニットテストの作成が非常に簡単になります。ストアを直接インポートし、その状態や関数をテストできます。

import useStore from './store'

test('increments the counter', () => {
  const { getState, setState } = useStore
  
  setState({ count: 0 })
  getState().increment()
  
  expect(getState().count).toBe(1)
})

Zustandと他の状態管理ライブラリの比較

Zustandの特徴をより深く理解するために、他の人気のある状態管理ライブラリと比較してみましょう。

Zustand vs Redux

  1. セットアップの簡易さ: Zustandは、Reduxと比べてセットアップがはるかに簡単です。Reduxでは、アクション、リデューサー、ストアの設定など、多くのボイラープレートコードが必要ですが、Zustandではそれらがすべて1つの関数で済みます。
  2. 学習曲線: Reduxには独自の概念(アクション、リデューサー、ディスパッチなど)があり、学習に時間がかかる場合がありますが、Zustandはほとんど純粋なJavaScriptの知識だけで使い始めることができます。
  3. パフォーマンス: 両者ともに高いパフォーマンスを発揮しますが、Zustandはより軽量で、小規模から中規模のアプリケーションでは特に効果を発揮します。
  4. ミドルウェアとエコシステム: Reduxは長い歴史を持ち、豊富なミドルウェアとエコシステムがありますが、Zustandも基本的な機能は網羅しており、多くの場合で十分です。

Zustand vs MobX

  1. アプローチ: MobXは観察可能な状態に基づいていますが、Zustandはより関数的なアプローチを取っています。
  2. ボイラープレート: MobXはデコレータを多用し、クラスベースのアプローチを取ることが多いですが、Zustandはよりシンプルな関数ベースのアプローチです。
  3. 透明性: Zustandは状態の変更がより明示的で追跡しやすいのに対し、MobXは「魔法のような」感覚を与えることがあります。
  4. 学習曲線: MobXは独自の概念(observable、computed、action など)があり、学習に時間がかかる場合がありますが、Zustandはより直感的です。

Zustand vs Recoil

  1. アプローチ: Recoilはアトムベースのアプローチを取り、状態を細かい単位で管理します。Zustandは1つのストアに全ての状態を置くことができます。
  2. セットアップ: RecoilはRecoilRootでアプリをラップする必要がありますが、Zustandにはそのような要件はありません。
  3. 非同期処理: Recoilは非同期処理のためのセレクタを提供していますが、Zustandでも非同期アクションを簡単に実装できます。
  4. 学習曲線: Recoilは新しい概念(アトム、セレクタ)を導入しており、学習に時間がかかる場合がありますが、Zustandはより直感的です。

Zustandの実践的な使用例

Zustandの理解をさらに深めるために、実際のユースケースを見ていきましょう。ここでは、簡単なTodoアプリケーションを例に取り、Zustandを使用して状態管理を行う方法を示します。

Todoアプリケーションの実装

まず、Todoアプリケーションのストアを作成します。

import create from 'zustand'

const useTodoStore = create((set) => ({
  todos: [],
  addTodo: (text) => set((state) => ({
    todos: [...state.todos, { id: Date.now(), text, 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)
  }))
}))

このストアは、Todoの追加、トグル(完了/未完了の切り替え)、削除の3つの操作を提供します。

次に、このストアを使用するReactコンポーネントを作成しましょう。

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

function TodoApp() {
  const [newTodo, setNewTodo] = useState('')
  const { todos, addTodo, toggleTodo, removeTodo } = useTodoStore()

  const handleSubmit = (e) => {
    e.preventDefault()
    if (newTodo.trim()) {
      addTodo(newTodo)
      setNewTodo('')
    }
  }

  return (
    <div>
      <h1>Todo List</h1>
      <form onSubmit={handleSubmit}>
        <input
          type="text"
          value={newTodo}
          onChange={(e) => setNewTodo(e.target.value)}
          placeholder="Add new todo"
        />
        <button type="submit">Add</button>
      </form>
      <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.text}
            </span>
            <button onClick={() => removeTodo(todo.id)}>Delete</button>
          </li>
        ))}
      </ul>
    </div>
  )
}

export default TodoApp

このコンポーネントでは、Zustandのストアから状態と操作を取得し、Todoの追加、完了状態の切り替え、削除を行うUIを提供しています。

Zustandを使用することで、状態管理のロジックをコンポーネントから分離し、再利用可能で管理しやすいコードを実現できました。このアプローチの利点は以下の通りです:

  1. シンプルさ: ストアの定義とその使用方法が直感的で理解しやすい。
  2. 柔軟性: 必要に応じて簡単にストアの機能を拡張できる。
  3. パフォーマンス: Zustandの内部最適化により、不要な再レンダリングを避けられる。
  4. テスト容易性: ストアのロジックを独立してテストできる。

Zustandのベストプラクティスとパターン

Zustandを効果的に使用するためのベストプラクティスとパターンをいくつか紹介します。

ストアの分割

アプリケーションが大きくなるにつれて、すべての状態を1つのストアで管理するのは難しくなります。そのような場合、ストアを機能ごとに分割することをお勧めします。

// userStore.js
import create from 'zustand'

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

// todoStore.js
import create from 'zustand'

const useTodoStore = create((set) => ({
  todos: [],
  addTodo: (todo) => set((state) => ({ todos: [...state.todos, todo] })),
  // ... 他のTodo関連の操作
}))

export { useUserStore, useTodoStore }

このアプローチにより、各ストアの責任が明確になり、コードの管理が容易になります。

セレクタの使用

パフォーマンスを最適化するために、セレクタを使用してストアの特定の部分のみを購読することをお勧めします。

const todoCount = useTodoStore(state => state.todos.length)
const incompleteTodos = useTodoStore(state => state.todos.filter(todo => !todo.completed))

セレクタを使用することで、関心のある状態の変更時のみコンポーネントが再レンダリングされるため、パフォーマンスが向上します。

非同期アクションのパターン

非同期操作を扱う際は、ローディング状態とエラー状態を管理するパターンを使用すると良いでしょう。

const useDataStore = create((set) => ({
  data: null,
  loading: false,
  error: null,
  fetchData: async () => {
    set({ loading: true, error: null })
    try {
      const response = await fetch('https://api.example.com/data')
      const data = await response.json()
      set({ data, loading: false })
    } catch (error) {
      set({ error, loading: false })
    }
  }
}))

このパターンを使用することで、UIでローディング状態やエラー状態を適切に表示できます。

イミュータブルな更新

状態を更新する際は、常にイミュータブルな方法で行うことが重要です。Zustandは自動的にイミュータブルな更新を行いますが、ネストされたオブジェクトを更新する際は注意が必要です。

const useStore = create((set) => ({
  user: { name: 'John', age: 30 },
  updateUserAge: (newAge) => set((state) => ({
    user: { ...state.user, age: newAge }
  }))
}))

このアプローチにより、予期せぬ副作用を避け、アプリケーションの予測可能性を高めることができます。

Zustandの発展的なトピック

Zustandの基本的な使い方を理解したところで、より高度なトピックについて探っていきましょう。

ミドルウェアの活用

Zustandは、ストアの動作をカスタマイズするためのミドルウェアをサポートしています。例えば、devtoolsミドルウェアを使用すると、Redux DevToolsと連携してデバッグを行うことができます。

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

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

他にも、persistミドルウェアを使用して状態をローカルストレージに永続化したり、immerミドルウェアを使用してより直感的な状態の更新を行ったりすることができます。

コンテキストとの統合

Zustandは、Reactのコンテキストと組み合わせて使用することもできます。これは、特定のコンポーネントツリーに対してのみストアを提供したい場合に便利です。

import React from 'react'
import create from 'zustand'
import { createContext, useContext } from 'react'

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

const StoreContext = createContext()

export const StoreProvider = ({ children }) => (
  <StoreContext.Provider value={createStore()}>{children}</StoreContext.Provider>
)

export const useStore = () => {
  const store = useContext(StoreContext)
  if (!store) {
    throw new Error('useStore must be used within a StoreProvider.')
  }
  return store
}

このアプローチにより、アプリケーションの特定の部分に対してのみストアを提供することができ、テストやコード分割が容易になります。

TypeScriptとの統合

TypeScriptを使用している場合、Zustandと組み合わせることで型安全な状態管理を実現できます。

import create from 'zustand'

interface BearState {
  bears: number
  increase: (by: number) => void
}

const useStore = create<BearState>((set) => ({
  bears: 0,
  increase: (by) => set((state) => ({ bears: state.bears + by })),
}))

TypeScriptを使用することで、開発時のエラーを事前に捕捉し、コードの品質を向上させることができます。

テスト戦略

Zustandを使用したアプリケーションのテストは比較的straightforwardです。ストアの動作を単体テストし、コンポーネントのテストではモックストアを使用することができます。

import create from 'zustand'

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

describe('counterStore', () => {
  it('should increment the count', () => {
    const store = createStore()
    store.getState().increment()
    expect(store.getState().count).toBe(1)
  })
})

コンポーネントのテストでは、jest.mock()を使用してストアをモックし、必要な状態と操作を提供することができます。

Zustandの未来と展望

Zustandは比較的新しいライブラリですが、その簡潔さと柔軟性により、Reactコミュニティで急速に人気を集めています。今後の展望としては以下のようなものが考えられます:

  1. エコシステムの成長: サードパーティのミドルウェアやツールの増加が期待されます。
  2. パフォーマンスの最適化: すでに高速ですが、さらなるパフォーマンスの向上が見込まれます。
  3. React以外のフレームワークとの統合: 現在はReact向けですが、将来的には他のフレームワークでも使用できるように拡張される可能性があります。
  4. サーバーサイドレンダリング(SSR)のサポート強化: SSRのサポートがさらに改善される可能性があります。

Zustandは、その簡潔さと柔軟性により、今後もReact開発者の間で人気を維持し、成長していくことが予想されます。

まとめ

Zustandは、Reactアプリケーションの状態管理を簡素化する強力なツールです。その主な利点は以下の通りです:

  1. シンプルなAPI: 学習コストが低く、すぐに使い始めることができます。
  2. 高いパフォーマンス: 軽量で効率的な実装により、アプリケーションのパフォーマンスを向上させます。
  3. 柔軟性: 小規模から大規模なプロジェクトまで、様々な規模のアプリケーションに対応できます。
  4. TypeScriptサポート: 型安全な開発を可能にします。
  5. ミドルウェアサポート: 機能を拡張し、開発体験を向上させることができます。

Zustandを使用することで、開発者はよりクリーンで管理しやすいコードを書くことができ、結果としてより高品質なReactアプリケーションを構築することができます。

状態管理ライブラリの選択は、プロジェクトの要件や個人の好みによって異なりますが、Zustandは多くの場合において優れた選択肢となるでしょう。特に、シンプルさと柔軟性を重視する開発者にとって、Zustandは魅力的なオプションとなるはずです。

Zustandの世界に飛び込んで、Reactアプリケーション開発の新たな可能性を探ってみてはいかがでしょうか?きっと、状態管理に対する考え方が変わり、より効率的で楽しい開発体験が待っているはずです。

Happy coding with Zustand! 🚀✨

参考リンク

  1. Zustand公式ドキュメント
  2. Zustandチュートリアル(英語)
  3. ZustandとReactパフォーマンス最適化(英語)

コメント

タイトルとURLをコピーしました