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>
This commit is contained in:
203
plugins/saas-react-platform/skills/state-patterns.md
Normal file
203
plugins/saas-react-platform/skills/state-patterns.md
Normal file
@@ -0,0 +1,203 @@
|
||||
---
|
||||
name: state-patterns
|
||||
description: 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
|
||||
|
||||
```typescript
|
||||
// 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
|
||||
|
||||
```typescript
|
||||
// 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
|
||||
|
||||
```typescript
|
||||
// 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)
|
||||
Reference in New Issue
Block a user