Zustandを使った状態管理を試してみた
はじめに
先日、Firebase Authでログインユーザーの状態管理を実装するタスクでuseContext
を使用していましたが、Zustand
の方が簡単に実装できると教えていただいたので初めて使ってみました。
フロントエンドの初心者なので、Zustand
の使い方に間違いがありましたらご指摘いただけますと幸いです。
Zustand
Zustand
は、Reactのための軽量な状態管理ライブラリです。
主な特徴:
- ボイラープレートが少ない
- TypeScriptのサポートが優れている
- 設定が極めてシンプルですぐに使用可能
Zustandの主要な概念
- 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
- State(状態)とアクション
- 状態(State): アプリケーションのデータを表現する部分です。
- アクション: 状態を更新するための関数です。
この分離により、データの流れが予測可能になり、デバッグが容易になります。
状態を定義する
type State = {
// 状態(データ)
count: number
user: User | null
isLoading: boolean
// アクション(状態を更新する関数)
increment: () => void
setUser: (user: User) => void
}
- 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 })
}
}
}))
- 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
を定義します。
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
を管理するフックを作成します。
ユーザー状態、ログインローディング状態、初期化状態を管理する変数と更新する関数を作成します。
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
で作成してます。
'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)に保存し、ページのリロードやページ遷移後も状態を維持するために使用されます。
persist
ミドルウェアを使用
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