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>
5.8 KiB
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
useStateorreact-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)