🦓

Zustandを使った状態管理を試してみた

2024/12/08に公開

はじめに

先日、Firebase Authでログインユーザーの状態管理を実装するタスクでuseContextを使用していましたが、Zustandの方が簡単に実装できると教えていただいたので初めて使ってみました。
フロントエンドの初心者なので、Zustandの使い方に間違いがありましたらご指摘いただけますと幸いです。

Zustand

Zustandは、Reactのための軽量な状態管理ライブラリです。

主な特徴:

  1. ボイラープレートが少ない
  2. TypeScriptのサポートが優れている
  3. 設定が極めてシンプルですぐに使用可能

https://zustand.docs.pmnd.rs/getting-started/introduction
https://github.com/pmndrs/zustand

Zustandの主要な概念

  1. Store(ストア)

ストアはアプリの状態を保持する場所です。
Zustandのストアは、状態(データ)とその状態を更新するためのアクション(関数)を含みます。create関数を使用して作成し、フック(useStore)として使用できます。

基本設定
// store.ts
import create from 'Zustand'

// ストアを定義する
type CounterStore = {
  count: number
  increment: () => void
  decrement: () => void
}

// useStoreフック
const useStore = create<CounterStore>((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 }))
}))

export default useStore
  1. State(状態)とアクション
  • 状態(State): アプリケーションのデータを表現する部分です。
  • アクション: 状態を更新するための関数です。
    この分離により、データの流れが予測可能になり、デバッグが容易になります。
状態を定義する
type State = {
  // 状態(データ)
  count: number
  user: User | null
  isLoading: boolean

  // アクション(状態を更新する関数)
  increment: () => void
  setUser: (user: User) => void
}
  1. set と get
  • set: 状態を更新するための関数です。直接更新や、前の状態を基にした更新が可能です。
  • get: 現在の状態を取得する関数です。他の状態に依存した更新を行う際に使用します。
setとget
const useStore = create((set, get) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),

  incrementIfEven: () => {
    const count = get().count
    if (count % 2 === 0) {
      set({ count: count + 1 })
    }
  }
}))
  1. Selector(セレクター)

セレクターを使用することで、必要な状態のみを購読でき、パフォーマンスが向上します。
不要な再レンダリングを防ぐことができます。

セレクター
function UserProfile() {
  // セレクターを使用して特定の状態のみを購読
  const username = useStore(state => state.user.name)
  const age = useStore(state => state.user.age)

  return (
    <div>
      <p>Name: {username}</p>
      <p>Age: {age}</p>
    </div>
  )
}

セットアップが簡単のと、状態管理の基本機能が揃っているため、
小〜中規模のアプリケーションで、シンプルな状態管理が必要な場合はZustandが適しています。


続いて、Firebase Authでログインしたユーザーを、ZustandとTypescriptを使って状態管理する流れを説明します。

tl:dr;

  • Zustandをインストールする
  • ログインユーザーを管理する
  • AuthStoreストアを使用してユーザー情報を表示
  • persistミドルウェアを使用

Zustandをインストールする

npm i zustand

ログインユーザーを管理する

ログインユーザーを管理するAuthStoreを定義します。

features/auth/stores/auth-store.ts
import { create } from 'zustand'
import { onAuthStateChanged } from 'firebase/auth'
import { auth } from '@/lib/firebase'
import { useAddUser } from '@/features/auth/api/use-add-user'


interface AuthStore {
  user: FirebaseUser | null // ユーザー
  loading: boolean // ローディング状態を表す変数
  initialized: boolean // 初期化状態を表す変数
  setLoading: (loading: boolean) => void // ローディング状態を変更する関数
  initialize: () => void // 状態の初期化
}

AuthStoreを管理するフックを作成します。
ユーザー状態、ログインローディング状態、初期化状態を管理する変数と更新する関数を作成します。

features/auth/stores/auth-store.ts
export const useAuthStore = create<AuthState>((set) => ({\
  // 初期状態を定義する
  user: null,
  loading: true,
  initialized: false,
  setLoading: (loading: boolean) => set({ loading }),

  initialize: () => {
    // Auth状態の変更を監視
    const unsubscribe = onAuthStateChanged(auth, async (user) => {
      if (user) {
        try {
          const userData: FirebaseUser = {
            id: user.uid,
            name: user.displayName || user.email,
            email: user.email,
            provider: user.providerData[0]?.providerId,
            photoUrl: user.photoURL || null,
            // ...
          }
          // 中略...
          // 状態を更新する
          set({
            user: userData,
            loading: false,
            initialized: true
          })
        } catch (error) {
          set({
            user: null,
            loading: false,
            initialized: true
          })
        }
      } else {
        set({
          user: null,
          loading: false,
          initialized: true
        })
      }
    })
    // アンマウント時のサブスクリプションのクリーンアップ
    return unsubscribe()
  },
}))

AuthStoreストアを使用してユーザー情報を表示

サイドバーコンポーネントからAuthStoreを呼び出してユーザー情報を表示させます。
サイドバーはshadcn/uiで作成してます。

components/layouts/app-sidebar.ts
'use client'
import { useEffect, useState } from 'react'
import Link from 'next/link'
import { useAuthStore } from '@/features/auth/stores/auth-store'
import Image from 'next/image'

export function AppSidebar() {
  // Authストア
  const { user } = useAuthStore()
  const [isLoading, setIsLoading] = useState(true)

  useEffect(() => {
    setIsLoading(false)
  }, [])

  if (isLoading) return null

  if (!user) return null

  return (
    <Sidebar collapsible="icon">
      <Image
        src={user.photoUrl}
        alt="User Avatar"
        width={32}
        height={32}
        className="rounded-full"
        style={{ objectFit: 'cover' }}
        unoptimized
      />
      <div className="flex items-center gap-3">
        <div className="flex flex-col">
          <span className="font-medium">
            {user?.name || user?.email?.split('@')[0]}
          </span>
          <span className="text-sm text-muted-foreground">
            {user?.email}
          </span>
        </div>
      </div>
    </Sidebar>
  )
}

ユーザーログインの動作確認を行ったところ、ログイン直後はサイドバーにユーザー名、メールアドレス、アイコンが正しく表示されましたが、他のページに遷移すると、サイドバーのユーザー情報が保持されていませんでした。

persistミドルウェアを使用する必要があるということでした。

persistミドルウェアは、Zustandの状態をブラウザのストレージ(LocalStorageやSessionStorage)に保存し、ページのリロードやページ遷移後も状態を維持するために使用されます。

https://zustand.docs.pmnd.rs/middlewares/persist#persist

persistミドルウェアを使用

features/auth/stores/auth-store.ts
import { create } from 'Zustand'
++ import { persist } from 'Zustand/middleware'
import { auth } from '@/lib/firebase'

// 中略...

export const useAuthStore = create<AuthState>()(
++ persist(
      (set) => ({
        user: null,
        initialized: false,
        initialize: async () => {
          return new Promise<void>((resolve) => {
            const unsubscribe = auth.onAuthStateChanged((firebaseUser) => {
              if (firebaseUser) {
                set({
                  user: {
                    id: firebaseUser.uid,
                    email: firebaseUser.email,
                    name: firebaseUser.displayName || firebaseUser.email,
                    provider: firebaseUser.providerData[0]?.providerId ||'',
                    photoUrl: firebaseUser.photoURL,
                  },
                  initialized: true,
                })
              } else {
                set({
                  user: null,
                  initialized: true
                })
              }
              resolve()
            })
            return () => unsubscribe()
          })
        },
      }),
++    {
++      name: 'auth-storage', // ストレージのキー名
++      partialize: (state) => ({user: state.user }), // ユーザーのみ永続化
++    },
++  ),
)

persistミドルウェアを使用し、ページ遷移後もユーザーデータが保持されるようになりました。
また、partializeを使用して必要な状態のみを永続化しています。

終わりに

Zustandを使って状態管理を実装してみました。

ログインユーザーの状態管理に重要なのは、ページ遷移やブラウザのリロード後もユーザーの状態を保持することです。
Zustandのpersistミドルウェアを使用して簡単に実現することができました。

このような実装により、保存する情報を必要最小限に制限し、ユーザーはログイン後にページ遷移しても、サイドバーなどのUIコンポーネントで一貫したユーザー情報を表示し続けることができました。

Discussion