Skip to content

Zustand完全ガイド

Zustandを使用した状態管理を、実務で使える実装例とベストプラクティスとともに詳しく解説します。

Zustandは、シンプルでミニマリストな状態管理ライブラリです。Reduxの代替として、より少ないボイラープレートで状態管理を実現します。

Zustandの特徴
├─ シンプルなAPI
├─ 少ないボイラープレート
├─ 高いパフォーマンス
└─ TypeScriptサポート

問題のある構成(Redux):

// 問題: ボイラープレートが多い
import { configureStore, createSlice } from '@reduxjs/toolkit';
import { Provider } from 'react-redux';
import { useSelector, useDispatch } from 'react-redux';
// Storeの作成
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: (state) => { state.value += 1; },
},
});
export const store = configureStore({
reducer: { counter: counterSlice.reducer },
});
// Providerの設定
function App() {
return (
<Provider store={store}>
<YourApp />
</Provider>
);
}
// 使用
function Counter() {
const count = useSelector((state) => state.counter.value);
const dispatch = useDispatch();
return <button onClick={() => dispatch(counterSlice.actions.increment())}>+</button>;
}

解決: Zustandによる簡潔な実装

// 解決: Zustandによる簡潔な実装
import { create } from 'zustand';
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}));
// 使用
function Counter() {
const count = useStore((state) => state.count);
const increment = useStore((state) => state.increment);
return <button onClick={increment}>+</button>;
}
Terminal window
npm install zustand
Terminal window
npm install zustand
# TypeScriptは標準でサポートされている
store/counterStore.js
import { create } from 'zustand';
const useCounterStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
}));
export default useCounterStore;
store/counterStore.ts
import { create } from 'zustand';
interface CounterState {
count: number;
increment: () => void;
decrement: () => void;
reset: () => void;
}
const useCounterStore = create<CounterState>((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
}));
export default useCounterStore;
store/userStore.js
import { create } from 'zustand';
const useUserStore = create((set) => ({
user: null,
loading: false,
error: null,
setUser: (user) => set({ user }),
setLoading: (loading) => set({ loading }),
setError: (error) => set({ error }),
fetchUser: async (userId) => {
set({ loading: true, error: null });
try {
const response = await fetch(`/api/users/${userId}`);
const user = await response.json();
set({ user, loading: false });
} catch (error) {
set({ error: error.message, loading: false });
}
},
}));
export default useUserStore;
components/Counter.jsx
import useCounterStore from '../store/counterStore';
function Counter() {
const count = useCounterStore((state) => state.count);
const increment = useCounterStore((state) => state.increment);
const decrement = useCounterStore((state) => state.decrement);
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
);
}
// パフォーマンス最適化: 必要な部分だけを選択
function Counter() {
// countが変更されたときだけ再レンダリング
const count = useCounterStore((state) => state.count);
return <div>{count}</div>;
}
function IncrementButton() {
// incrementが変更されたときだけ再レンダリング(実際には変更されない)
const increment = useCounterStore((state) => state.increment);
return <button onClick={increment}>+</button>;
}
// 複数の値を一度に選択
function UserProfile() {
const { user, loading, error } = useUserStore((state) => ({
user: state.user,
loading: state.loading,
error: state.error,
}));
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return <div>{user?.name}</div>;
}
store/counterStore.js
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
const useCounterStore = create(
devtools(
(set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}),
{ name: 'CounterStore' }
)
);
store/userStore.js
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
const useUserStore = create(
persist(
(set) => ({
user: null,
setUser: (user) => set({ user }),
}),
{
name: 'user-storage', // localStorageのキー名
}
)
);
store/counterStore.js
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
const useCounterStore = create(
devtools(
persist(
(set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}),
{ name: 'counter-storage' }
),
{ name: 'CounterStore' }
)
);

6. 実務でのベストプラクティス

Section titled “6. 実務でのベストプラクティス”
store/userStore.js
import { create } from 'zustand';
const useUserStore = create((set) => ({
user: null,
setUser: (user) => set({ user }),
}));
// store/themeStore.js
import { create } from 'zustand';
const useThemeStore = create((set) => ({
theme: 'light',
setTheme: (theme) => set({ theme }),
}));
// 使用
function App() {
const user = useUserStore((state) => state.user);
const theme = useThemeStore((state) => state.theme);
return <div className={theme}>{user?.name}</div>;
}

パターン2: カスタムフックの作成

Section titled “パターン2: カスタムフックの作成”
hooks/useUser.js
import useUserStore from '../store/userStore';
export function useUser() {
const user = useUserStore((state) => state.user);
const setUser = useUserStore((state) => state.setUser);
const fetchUser = useUserStore((state) => state.fetchUser);
return { user, setUser, fetchUser };
}
// 使用
function UserProfile() {
const { user, fetchUser } = useUser();
useEffect(() => {
fetchUser(userId);
}, [userId]);
return <div>{user?.name}</div>;
}
store/userStore.js
import { create } from 'zustand';
const useUserStore = create((set, get) => ({
user: null,
loading: false,
error: null,
fetchUser: async (userId) => {
set({ loading: true, error: null });
try {
const response = await fetch(`/api/users/${userId}`);
const user = await response.json();
set({ user, loading: false });
} catch (error) {
set({ error: error.message, loading: false });
}
},
updateUser: async (userId, data) => {
set({ loading: true });
try {
const response = await fetch(`/api/users/${userId}`, {
method: 'PUT',
body: JSON.stringify(data),
});
const user = await response.json();
set({ user, loading: false });
} catch (error) {
set({ error: error.message, loading: false });
}
},
}));

原因:

  • オブジェクト全体を選択している
  • 浅い比較が機能していない

解決策:

// 問題: オブジェクト全体を選択
const { user, loading } = useUserStore((state) => ({
user: state.user,
loading: state.loading,
}));
// 解決: 個別に選択
const user = useUserStore((state) => state.user);
const loading = useUserStore((state) => state.loading);
// または、shallowを使用
import { shallow } from 'zustand/shallow';
const { user, loading } = useUserStore(
(state) => ({ user: state.user, loading: state.loading }),
shallow
);

原因:

  • TypeScriptの型定義が不十分

解決策:

// 適切な型定義
interface UserState {
user: User | null;
setUser: (user: User) => void;
}
const useUserStore = create<UserState>((set) => ({
user: null,
setUser: (user) => set({ user }),
}));

これで、Zustandの基礎知識と実務での使い方を理解できるようになりました。