mirror of
https://github.com/wshobson/agents.git
synced 2026-03-18 09:37:15 +00:00
feat: add 5 new specialized agents with 20 skills
Add domain expert agents with comprehensive skill sets: - service-mesh-expert (cloud-infrastructure): Istio/Linkerd patterns, mTLS, observability - event-sourcing-architect (backend-development): CQRS, event stores, projections, sagas - vector-database-engineer (llm-application-dev): embeddings, similarity search, hybrid search - monorepo-architect (developer-essentials): Nx, Turborepo, Bazel, pnpm workspaces - threat-modeling-expert (security-scanning): STRIDE, attack trees, security requirements Update all documentation to reflect correct counts: - 67 plugins, 99 agents, 107 skills, 71 commands
This commit is contained in:
@@ -0,0 +1,429 @@
|
||||
---
|
||||
name: react-state-management
|
||||
description: Master modern React state management with Redux Toolkit, Zustand, Jotai, and React Query. Use when setting up global state, managing server state, or choosing between state management solutions.
|
||||
---
|
||||
|
||||
# React State Management
|
||||
|
||||
Comprehensive guide to modern React state management patterns, from local component state to global stores and server state synchronization.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- Setting up global state management in a React app
|
||||
- Choosing between Redux Toolkit, Zustand, or Jotai
|
||||
- Managing server state with React Query or SWR
|
||||
- Implementing optimistic updates
|
||||
- Debugging state-related issues
|
||||
- Migrating from legacy Redux to modern patterns
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### 1. State Categories
|
||||
|
||||
| Type | Description | Solutions |
|
||||
|------|-------------|-----------|
|
||||
| **Local State** | Component-specific, UI state | useState, useReducer |
|
||||
| **Global State** | Shared across components | Redux Toolkit, Zustand, Jotai |
|
||||
| **Server State** | Remote data, caching | React Query, SWR, RTK Query |
|
||||
| **URL State** | Route parameters, search | React Router, nuqs |
|
||||
| **Form State** | Input values, validation | React Hook Form, Formik |
|
||||
|
||||
### 2. Selection Criteria
|
||||
|
||||
```
|
||||
Small app, simple state → Zustand or Jotai
|
||||
Large app, complex state → Redux Toolkit
|
||||
Heavy server interaction → React Query + light client state
|
||||
Atomic/granular updates → Jotai
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Zustand (Simplest)
|
||||
|
||||
```typescript
|
||||
// store/useStore.ts
|
||||
import { create } from 'zustand'
|
||||
import { devtools, persist } from 'zustand/middleware'
|
||||
|
||||
interface AppState {
|
||||
user: User | null
|
||||
theme: 'light' | 'dark'
|
||||
setUser: (user: User | null) => void
|
||||
toggleTheme: () => void
|
||||
}
|
||||
|
||||
export const useStore = create<AppState>()(
|
||||
devtools(
|
||||
persist(
|
||||
(set) => ({
|
||||
user: null,
|
||||
theme: 'light',
|
||||
setUser: (user) => set({ user }),
|
||||
toggleTheme: () => set((state) => ({
|
||||
theme: state.theme === 'light' ? 'dark' : 'light'
|
||||
})),
|
||||
}),
|
||||
{ name: 'app-storage' }
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
// Usage in component
|
||||
function Header() {
|
||||
const { user, theme, toggleTheme } = useStore()
|
||||
return (
|
||||
<header className={theme}>
|
||||
{user?.name}
|
||||
<button onClick={toggleTheme}>Toggle Theme</button>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Patterns
|
||||
|
||||
### Pattern 1: Redux Toolkit with TypeScript
|
||||
|
||||
```typescript
|
||||
// store/index.ts
|
||||
import { configureStore } from '@reduxjs/toolkit'
|
||||
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
|
||||
import userReducer from './slices/userSlice'
|
||||
import cartReducer from './slices/cartSlice'
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
user: userReducer,
|
||||
cart: cartReducer,
|
||||
},
|
||||
middleware: (getDefaultMiddleware) =>
|
||||
getDefaultMiddleware({
|
||||
serializableCheck: {
|
||||
ignoredActions: ['persist/PERSIST'],
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
export type RootState = ReturnType<typeof store.getState>
|
||||
export type AppDispatch = typeof store.dispatch
|
||||
|
||||
// Typed hooks
|
||||
export const useAppDispatch: () => AppDispatch = useDispatch
|
||||
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
|
||||
```
|
||||
|
||||
```typescript
|
||||
// store/slices/userSlice.ts
|
||||
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'
|
||||
|
||||
interface User {
|
||||
id: string
|
||||
email: string
|
||||
name: string
|
||||
}
|
||||
|
||||
interface UserState {
|
||||
current: User | null
|
||||
status: 'idle' | 'loading' | 'succeeded' | 'failed'
|
||||
error: string | null
|
||||
}
|
||||
|
||||
const initialState: UserState = {
|
||||
current: null,
|
||||
status: 'idle',
|
||||
error: null,
|
||||
}
|
||||
|
||||
export const fetchUser = createAsyncThunk(
|
||||
'user/fetchUser',
|
||||
async (userId: string, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await fetch(`/api/users/${userId}`)
|
||||
if (!response.ok) throw new Error('Failed to fetch user')
|
||||
return await response.json()
|
||||
} catch (error) {
|
||||
return rejectWithValue((error as Error).message)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const userSlice = createSlice({
|
||||
name: 'user',
|
||||
initialState,
|
||||
reducers: {
|
||||
setUser: (state, action: PayloadAction<User>) => {
|
||||
state.current = action.payload
|
||||
state.status = 'succeeded'
|
||||
},
|
||||
clearUser: (state) => {
|
||||
state.current = null
|
||||
state.status = 'idle'
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
.addCase(fetchUser.pending, (state) => {
|
||||
state.status = 'loading'
|
||||
state.error = null
|
||||
})
|
||||
.addCase(fetchUser.fulfilled, (state, action) => {
|
||||
state.status = 'succeeded'
|
||||
state.current = action.payload
|
||||
})
|
||||
.addCase(fetchUser.rejected, (state, action) => {
|
||||
state.status = 'failed'
|
||||
state.error = action.payload as string
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
export const { setUser, clearUser } = userSlice.actions
|
||||
export default userSlice.reducer
|
||||
```
|
||||
|
||||
### Pattern 2: Zustand with Slices (Scalable)
|
||||
|
||||
```typescript
|
||||
// store/slices/createUserSlice.ts
|
||||
import { StateCreator } from 'zustand'
|
||||
|
||||
export interface UserSlice {
|
||||
user: User | null
|
||||
isAuthenticated: boolean
|
||||
login: (credentials: Credentials) => Promise<void>
|
||||
logout: () => void
|
||||
}
|
||||
|
||||
export const createUserSlice: StateCreator<
|
||||
UserSlice & CartSlice, // Combined store type
|
||||
[],
|
||||
[],
|
||||
UserSlice
|
||||
> = (set, get) => ({
|
||||
user: null,
|
||||
isAuthenticated: false,
|
||||
login: async (credentials) => {
|
||||
const user = await authApi.login(credentials)
|
||||
set({ user, isAuthenticated: true })
|
||||
},
|
||||
logout: () => {
|
||||
set({ user: null, isAuthenticated: false })
|
||||
// Can access other slices
|
||||
// get().clearCart()
|
||||
},
|
||||
})
|
||||
|
||||
// store/index.ts
|
||||
import { create } from 'zustand'
|
||||
import { createUserSlice, UserSlice } from './slices/createUserSlice'
|
||||
import { createCartSlice, CartSlice } from './slices/createCartSlice'
|
||||
|
||||
type StoreState = UserSlice & CartSlice
|
||||
|
||||
export const useStore = create<StoreState>()((...args) => ({
|
||||
...createUserSlice(...args),
|
||||
...createCartSlice(...args),
|
||||
}))
|
||||
|
||||
// Selective subscriptions (prevents unnecessary re-renders)
|
||||
export const useUser = () => useStore((state) => state.user)
|
||||
export const useCart = () => useStore((state) => state.cart)
|
||||
```
|
||||
|
||||
### Pattern 3: Jotai for Atomic State
|
||||
|
||||
```typescript
|
||||
// atoms/userAtoms.ts
|
||||
import { atom } from 'jotai'
|
||||
import { atomWithStorage } from 'jotai/utils'
|
||||
|
||||
// Basic atom
|
||||
export const userAtom = atom<User | null>(null)
|
||||
|
||||
// Derived atom (computed)
|
||||
export const isAuthenticatedAtom = atom((get) => get(userAtom) !== null)
|
||||
|
||||
// Atom with localStorage persistence
|
||||
export const themeAtom = atomWithStorage<'light' | 'dark'>('theme', 'light')
|
||||
|
||||
// Async atom
|
||||
export const userProfileAtom = atom(async (get) => {
|
||||
const user = get(userAtom)
|
||||
if (!user) return null
|
||||
const response = await fetch(`/api/users/${user.id}/profile`)
|
||||
return response.json()
|
||||
})
|
||||
|
||||
// Write-only atom (action)
|
||||
export const logoutAtom = atom(null, (get, set) => {
|
||||
set(userAtom, null)
|
||||
set(cartAtom, [])
|
||||
localStorage.removeItem('token')
|
||||
})
|
||||
|
||||
// Usage
|
||||
function Profile() {
|
||||
const [user] = useAtom(userAtom)
|
||||
const [, logout] = useAtom(logoutAtom)
|
||||
const [profile] = useAtom(userProfileAtom) // Suspense-enabled
|
||||
|
||||
return (
|
||||
<Suspense fallback={<Skeleton />}>
|
||||
<ProfileContent profile={profile} onLogout={logout} />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 4: React Query for Server State
|
||||
|
||||
```typescript
|
||||
// hooks/useUsers.ts
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
|
||||
// Query keys factory
|
||||
export const userKeys = {
|
||||
all: ['users'] as const,
|
||||
lists: () => [...userKeys.all, 'list'] as const,
|
||||
list: (filters: UserFilters) => [...userKeys.lists(), filters] as const,
|
||||
details: () => [...userKeys.all, 'detail'] as const,
|
||||
detail: (id: string) => [...userKeys.details(), id] as const,
|
||||
}
|
||||
|
||||
// Fetch hook
|
||||
export function useUsers(filters: UserFilters) {
|
||||
return useQuery({
|
||||
queryKey: userKeys.list(filters),
|
||||
queryFn: () => fetchUsers(filters),
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
gcTime: 30 * 60 * 1000, // 30 minutes (formerly cacheTime)
|
||||
})
|
||||
}
|
||||
|
||||
// Single user hook
|
||||
export function useUser(id: string) {
|
||||
return useQuery({
|
||||
queryKey: userKeys.detail(id),
|
||||
queryFn: () => fetchUser(id),
|
||||
enabled: !!id, // Don't fetch if no id
|
||||
})
|
||||
}
|
||||
|
||||
// Mutation with optimistic update
|
||||
export function useUpdateUser() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: updateUser,
|
||||
onMutate: async (newUser) => {
|
||||
// Cancel outgoing refetches
|
||||
await queryClient.cancelQueries({ queryKey: userKeys.detail(newUser.id) })
|
||||
|
||||
// Snapshot previous value
|
||||
const previousUser = queryClient.getQueryData(userKeys.detail(newUser.id))
|
||||
|
||||
// Optimistically update
|
||||
queryClient.setQueryData(userKeys.detail(newUser.id), newUser)
|
||||
|
||||
return { previousUser }
|
||||
},
|
||||
onError: (err, newUser, context) => {
|
||||
// Rollback on error
|
||||
queryClient.setQueryData(
|
||||
userKeys.detail(newUser.id),
|
||||
context?.previousUser
|
||||
)
|
||||
},
|
||||
onSettled: (data, error, variables) => {
|
||||
// Refetch after mutation
|
||||
queryClient.invalidateQueries({ queryKey: userKeys.detail(variables.id) })
|
||||
},
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 5: Combining Client + Server State
|
||||
|
||||
```typescript
|
||||
// Zustand for client state
|
||||
const useUIStore = create<UIState>((set) => ({
|
||||
sidebarOpen: true,
|
||||
modal: null,
|
||||
toggleSidebar: () => set((s) => ({ sidebarOpen: !s.sidebarOpen })),
|
||||
openModal: (modal) => set({ modal }),
|
||||
closeModal: () => set({ modal: null }),
|
||||
}))
|
||||
|
||||
// React Query for server state
|
||||
function Dashboard() {
|
||||
const { sidebarOpen, toggleSidebar } = useUIStore()
|
||||
const { data: users, isLoading } = useUsers({ active: true })
|
||||
const { data: stats } = useStats()
|
||||
|
||||
if (isLoading) return <DashboardSkeleton />
|
||||
|
||||
return (
|
||||
<div className={sidebarOpen ? 'with-sidebar' : ''}>
|
||||
<Sidebar open={sidebarOpen} onToggle={toggleSidebar} />
|
||||
<main>
|
||||
<StatsCards stats={stats} />
|
||||
<UserTable users={users} />
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Do's
|
||||
- **Colocate state** - Keep state as close to where it's used as possible
|
||||
- **Use selectors** - Prevent unnecessary re-renders with selective subscriptions
|
||||
- **Normalize data** - Flatten nested structures for easier updates
|
||||
- **Type everything** - Full TypeScript coverage prevents runtime errors
|
||||
- **Separate concerns** - Server state (React Query) vs client state (Zustand)
|
||||
|
||||
### Don'ts
|
||||
- **Don't over-globalize** - Not everything needs to be in global state
|
||||
- **Don't duplicate server state** - Let React Query manage it
|
||||
- **Don't mutate directly** - Always use immutable updates
|
||||
- **Don't store derived data** - Compute it instead
|
||||
- **Don't mix paradigms** - Pick one primary solution per category
|
||||
|
||||
## Migration Guides
|
||||
|
||||
### From Legacy Redux to RTK
|
||||
|
||||
```typescript
|
||||
// Before (legacy Redux)
|
||||
const ADD_TODO = 'ADD_TODO'
|
||||
const addTodo = (text) => ({ type: ADD_TODO, payload: text })
|
||||
function todosReducer(state = [], action) {
|
||||
switch (action.type) {
|
||||
case ADD_TODO:
|
||||
return [...state, { text: action.payload, completed: false }]
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
// After (Redux Toolkit)
|
||||
const todosSlice = createSlice({
|
||||
name: 'todos',
|
||||
initialState: [],
|
||||
reducers: {
|
||||
addTodo: (state, action: PayloadAction<string>) => {
|
||||
// Immer allows "mutations"
|
||||
state.push({ text: action.payload, completed: false })
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- [Redux Toolkit Documentation](https://redux-toolkit.js.org/)
|
||||
- [Zustand GitHub](https://github.com/pmndrs/zustand)
|
||||
- [Jotai Documentation](https://jotai.org/)
|
||||
- [TanStack Query](https://tanstack.com/query)
|
||||
Reference in New Issue
Block a user