Files
leo-claude-mktplace/plugins/saas-react-platform/skills/state-patterns.md
lmiranda 2d51df7a42 feat(marketplace): command consolidation + 8 new plugins (v8.1.0 → v9.0.0) [BREAKING]
Phase 1b: Rename all ~94 commands across 12 plugins to /<noun> <action>
sub-command pattern. Git-flow consolidated from 8→5 commands (commit
variants absorbed into --push/--merge/--sync flags). Dispatch files,
name: frontmatter, and cross-reference updates for all plugins.

Phase 2: Design documents for 8 new plugins in docs/designs/.

Phase 3: Scaffold 8 new plugins — saas-api-platform, saas-db-migrate,
saas-react-platform, saas-test-pilot, data-seed, ops-release-manager,
ops-deploy-pipeline, debug-mcp. Each with plugin.json, commands, agents,
skills, README, and claude-md-integration. Marketplace grows from 12→20.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 14:52:11 -05:00

5.8 KiB

name, description
name description
state-patterns State management patterns — React Context for simple, Zustand for medium, Redux Toolkit for complex

State Management Patterns

Purpose

Guide state management decisions and provide scaffolding templates for React Context, Zustand, and Redux Toolkit. This skill helps select the right pattern based on complexity and generates consistent store implementations.


Decision Framework

Criteria Context Zustand Redux Toolkit
Scope Single feature, few consumers Multiple features, medium consumers App-wide, many consumers
Complexity Simple values (theme, locale, auth) Medium (cart, form wizard, filters) Complex (normalized entities, async workflows)
Async logic Manual with useEffect Built-in with async actions createAsyncThunk with lifecycle
DevTools None built-in Optional middleware Full Redux DevTools integration
Dependencies None (built-in React) ~2KB, zero config ~12KB, more boilerplate
Learning curve Low Low Medium-High

Quick Decision

  • Need to share a simple value across a few components? Context
  • Need a store with some async logic and moderate complexity? Zustand
  • Need normalized state, middleware, complex async flows, or strict patterns? Redux Toolkit

React Context Template

// stores/auth-context.tsx
import { createContext, useContext, useReducer, type ReactNode } from 'react';

// State type
interface AuthState {
  user: User | null;
  isAuthenticated: boolean;
  isLoading: boolean;
}

// Action types
type AuthAction =
  | { type: 'LOGIN'; payload: User }
  | { type: 'LOGOUT' }
  | { type: 'SET_LOADING'; payload: boolean };

// Initial state
const initialState: AuthState = {
  user: null,
  isAuthenticated: false,
  isLoading: false,
};

// Reducer
function authReducer(state: AuthState, action: AuthAction): AuthState {
  switch (action.type) {
    case 'LOGIN':
      return { ...state, user: action.payload, isAuthenticated: true, isLoading: false };
    case 'LOGOUT':
      return { ...state, user: null, isAuthenticated: false };
    case 'SET_LOADING':
      return { ...state, isLoading: action.payload };
    default:
      return state;
  }
}

// Context
const AuthContext = createContext<{
  state: AuthState;
  dispatch: React.Dispatch<AuthAction>;
} | null>(null);

// Provider
export function AuthProvider({ children }: { children: ReactNode }) {
  const [state, dispatch] = useReducer(authReducer, initialState);
  return (
    <AuthContext.Provider value={{ state, dispatch }}>
      {children}
    </AuthContext.Provider>
  );
}

// Hook with validation
export function useAuth() {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth must be used within AuthProvider');
  }
  return context;
}

Zustand Template

// stores/cart-store.ts
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';

interface CartItem {
  id: string;
  name: string;
  price: number;
  quantity: number;
}

interface CartState {
  items: CartItem[];
  addItem: (item: Omit<CartItem, 'quantity'>) => void;
  removeItem: (id: string) => void;
  clearCart: () => void;
  totalPrice: () => number;
}

export const useCartStore = create<CartState>()(
  devtools(
    persist(
      (set, get) => ({
        items: [],
        addItem: (item) => set((state) => {
          const existing = state.items.find((i) => i.id === item.id);
          if (existing) {
            return { items: state.items.map((i) =>
              i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
            )};
          }
          return { items: [...state.items, { ...item, quantity: 1 }] };
        }),
        removeItem: (id) => set((state) => ({
          items: state.items.filter((i) => i.id !== id),
        })),
        clearCart: () => set({ items: [] }),
        totalPrice: () => get().items.reduce(
          (sum, item) => sum + item.price * item.quantity, 0
        ),
      }),
      { name: 'cart-storage' }
    )
  )
);

Redux Toolkit Template

// store/slices/productsSlice.ts
import { createSlice, createAsyncThunk, type PayloadAction } from '@reduxjs/toolkit';

// Async thunk
export const fetchProducts = createAsyncThunk(
  'products/fetchAll',
  async (_, { rejectWithValue }) => {
    try {
      const response = await fetch('/api/products');
      return await response.json();
    } catch (error) {
      return rejectWithValue('Failed to fetch products');
    }
  }
);

// Slice
const productsSlice = createSlice({
  name: 'products',
  initialState: {
    items: [] as Product[],
    status: 'idle' as 'idle' | 'loading' | 'succeeded' | 'failed',
    error: null as string | null,
  },
  reducers: {
    updateProduct: (state, action: PayloadAction<Product>) => {
      const index = state.items.findIndex((p) => p.id === action.payload.id);
      if (index !== -1) state.items[index] = action.payload;
    },
  },
  extraReducers: (builder) => {
    builder
      .addCase(fetchProducts.pending, (state) => { state.status = 'loading'; })
      .addCase(fetchProducts.fulfilled, (state, action) => {
        state.status = 'succeeded';
        state.items = action.payload;
      })
      .addCase(fetchProducts.rejected, (state, action) => {
        state.status = 'failed';
        state.error = action.payload as string;
      });
  },
});

export const { updateProduct } = productsSlice.actions;
export default productsSlice.reducer;

When NOT to Use Global State

  • Form input values (use local useState or react-hook-form)
  • UI toggle state (modal open/close) unless shared across routes
  • Computed values derivable from existing state (compute inline or useMemo)
  • Server cache data (use TanStack Query or SWR instead of Redux)