Enterprise State Management: React & Zustand 🐻 のカバー画像

Prajapati Paresh

React Context の崩壊

ステート管理は、React エコシステムで最も活発に議論されるトピックの一つです。長年 Redux が絶対的な王者でしたが、その膨大なボイラープレートと急な学習曲線により、多くのチームが代替手段を求めるようになりました。React が Context API を導入したとき、多くの開発者はそれが究極のステート管理の置き換えになると考えました。しかし、エンタープライズアプリケーションにおいて、Context を急速に変化するステートに使用すると、2 つの重大なアーキテクチャ上の失敗を招きます:Provider Hell不必要な再レンダリング です。

Context はアプリケーションを <Provider> タグの層でラップすることを強いるため、コンポーネントツリーが深くネストされ、読みづらくなります。さらに悪いことに、Context オブジェクト内の単一の値が更新されるたびに、その Context を利用するすべてのコンポーネントが再レンダリングを強制され、同じ Context 内の完全に無関係なデータのみに関心がある場合でも同様です。

Smart Tech Devs では、重量級の Redux ボイラープレートを放棄し、Context API の落とし穴を避けることで、高性能な React および Next.js アプリケーションを設計しています。代わりに Zustand を使用します。Zustand は最小限で意見を持たず、非常に高速なステート管理ライブラリであり、React コンポーネントツリーの外側で動作するため、Provider Hell を完全に排除します。

Zustand の哲学

Context とは異なり、Zustand はアトミックでサブスクリプションベースのモデルに依存しています。ステートは React のレンダリングサイクルの外側にある集中型ストアに存在します。コンポーネントは「セレクター」を使用して、必要な状態の正確なスライスのみにサブスクライブします。プロパティが更新された場合、その特定のプロパティに明示的にサブスクライブしているコンポーネントのみが再レンダリングされます。

ステップ 1: ストアのアーキテクチャ(スライスパターン)

大規模なアプリケーションでは、すべてのステートを 1 つの巨大なファイルに置くことはアンチパターンです。Zustand では「スライス」パターンを使用してステートをアーキテクチャ化できます。ドメインごとに論理的にストアを分割し(例: Auth、Cart、UI)、それらをマージします。


// store/useStore.ts
import { create } from 'zustand';

// Define the shape of our User slice
interface AuthSlice {
  user: { id: string; name: string } | null;
  login: (userData: { id: string; name: string }) => void;
  logout: () => void;
}

// Define the shape of our Cart slice
interface CartSlice {
  items: Array<{ id: string; price: number }>;
  addItem: (item: { id: string; price: number }) => void;
  clearCart: () => void;
}

// Combine the types
type StoreState = AuthSlice & CartSlice;

// Create the unified store using the set function
export const useStore = create()((set) => ({
  // Auth Slice Implementation
  user: null,
  login: (userData) => set(() => ({ user: userData })),
  logout: () => set(() => ({ user: null })),

  // Cart Slice Implementation
  items: [],
  addItem: (item) => set((state) => ({ items: [...state.items, item] })),
  clearCart: () => set(() => ({ items: [] })),
}));

ステップ 2: セレクターによる再レンダリングの防止

Zustand の真の力は、ステートを利用するときに明らかになります。<Provider> ラッパーが存在しないため、フックをどこにでもインポートできます。ただし、厳密なレンダリング境界を確保するには セレクター を使用する必要があります。

コンポーネントが login 関数のみを必要とする場合、カートにアイテムが追加されても再レンダリングすべきではありません。


// components/LoginButton.tsx
import { useStore } from '@/store/useStore';

export default function LoginButton() {
  // ✅ CORRECT: Selecting only the specific function.
  // This component will NEVER re-render when cart items change.
  const login = useStore((state) => state.login);

  return (
     login({ id: '1', name: 'John Doe' })}
      className="bg-blue-600 text-white px-4 py-2 rounded"
    >
      Sign In
    
  );
}

これを React Context アプローチと比較すると、useContext(StoreContext) にアクセスすると、カート合計が更新されるたびに LoginButton が再レンダリングを強制されます。Zustand はこれをネイティブに解決します。

ステップ 3: エンタープライズ向けミドルウェア(永続化)

エンタープライズアプリケーションでは、ページリロードをまたいだステートの永続化が必要です(例: ユーザーのカートを保持する)。Redux では redux-persist を用いた複雑な設定が必要ですが、Zustand では組み込みのミドルウェアで即座に実現できます。


// store/useStore.ts (Refactored with Persist Middleware)
import { create } from 'zustand';
import { persist } from 'zustand/middleware';

export const useStore = create()(
  persist(
    (set) => ({
      user: null,
      login: (userData) => set(() => ({ user: userData })),
      // ... rest of state
    }),
    {
      name: 'smart-tech-storage', // Key name in localStorage
      partialize: (state) => ({ user: state.user }), // Only persist the user slice!
    }
  )
);

エンジニアリング上の ROI

Zustand とスライスパターンを採用することで、React Provider Hell を即座に解消し、コンポーネントツリーを平坦化し、開発者体験を大幅に向上させます。ステートロジックが React のレンダリングライフサイクルから切り離されているため、React コンポーネントの外側(標準的なユーティリティ関数や Axios インターセプター内など)からもストアにアクセスして変更できます。さらに、セレクターを厳密に利用することで、レンダリングパフォーマンスを最適化し、ステートオブジェクトが数千のプロパティに拡大してもフロントエンドが非常に高速に保たれます。