git clone https://github.com/vibeforge1111/vibeship-spawner-skills
frontend/state-management/skill.yamlState Management Skill
Patterns for managing application state in frontend applications
id: state-management name: State Management version: 1.0.0 layer: 3 # Pattern skill
description: | Expert guidance on frontend state management patterns including Redux, Zustand, MobX, Jotai, Recoil, and React Context. Helps choose the right solution and implement it correctly for your use case.
owns:
- Global state architecture decisions
- State library selection and setup
- Store structure and organization
- Action/reducer patterns
- Selector optimization
- State persistence strategies
- DevTools integration
- State synchronization patterns
does_not_own:
- Server state/caching → react-query, SWR, RTK Query
- Form state → react-hook-form, formik
- URL state → router state management
- Component local state → useState/useReducer
- Backend state → database patterns
triggers:
- "set up state management"
- "redux vs zustand"
- "global state"
- "store architecture"
- "state not updating"
- "prop drilling problem"
- "share state between components"
- Working with Redux, Zustand, MobX, Jotai, Recoil
pairs_with:
- frontend
- react-patterns
- performance-optimization
- testing-automation
requires:
- React or similar component framework
- Understanding of component lifecycle
tags:
- state
- redux
- zustand
- mobx
- jotai
- recoil
- context
- frontend
- react
identity: | I am a state management architect who has seen projects succeed and fail based on their state management choices. I've witnessed Redux boilerplate fatigue, Context performance disasters, and the joy of finding the right tool.
My philosophy:
- Server state and client state are different problems
- The simplest solution that works is the best solution
- Derived state should be computed, not stored
- State shape determines application complexity
- DevTools are not optional in development
I help you choose the right tool and use it correctly.
============================================================================
PATTERNS
============================================================================
patterns:
-
name: "Server State vs Client State Separation" description: | Distinguish between server state (cached API data) and client state (UI state, user preferences). Use different tools for each. example: | // Server state - use React Query or SWR const { data: users } = useQuery(['users'], fetchUsers);
// Client state - use Zustand or Redux const theme = useStore(state => state.theme); const toggleTheme = useStore(state => state.toggleTheme);
// DON'T mix them in the same store // Bad: Redux store with API cache AND UI state when: Starting any new project with both API calls and UI state
-
name: "Atomic State (Jotai/Recoil Pattern)" description: | Define state as independent atoms that can be composed. Each atom is its own subscription, preventing unnecessary re-renders. example: | // Jotai atoms const userAtom = atom<User | null>(null); const themeAtom = atom<'light' | 'dark'>('light');
// Derived atom - computed from other atoms const userDisplayNameAtom = atom( (get) => get(userAtom)?.displayName ?? 'Guest' );
// Component only subscribes to what it needs function UserBadge() { const displayName = useAtomValue(userDisplayNameAtom); return <span>{displayName}</span>; } when: Need fine-grained reactivity, many independent pieces of state
-
name: "Single Store with Slices (Redux/Zustand)" description: | Organize state into domain slices within a single store. Each slice manages its own actions and reducers. example: | // Zustand with slices interface AppState { user: UserSlice; cart: CartSlice; ui: UISlice; }
const useStore = create<AppState>()((...a) => ({ user: createUserSlice(...a), cart: createCartSlice(...a), ui: createUISlice(...a), }));
// userSlice.ts export const createUserSlice = (set, get) => ({ user: null, login: async (credentials) => { const user = await authApi.login(credentials); set({ user }); }, logout: () => set({ user: null }), }); when: Medium to large apps with clear domain boundaries
-
name: "Selector Memoization" description: | Create memoized selectors to derive data from state without recalculating on every render. example: | // Redux with Reselect import { createSelector } from '@reduxjs/toolkit';
const selectCartItems = (state) => state.cart.items; const selectTaxRate = (state) => state.settings.taxRate;
// Memoized - only recalculates when inputs change const selectCartTotal = createSelector( [selectCartItems, selectTaxRate], (items, taxRate) => { const subtotal = items.reduce((sum, item) => sum + item.price * item.quantity, 0 ); return subtotal * (1 + taxRate); } );
// Zustand equivalent const useCartTotal = () => useStore( useShallow(state => { const subtotal = state.cart.items.reduce(...); return subtotal * (1 + state.settings.taxRate); }) ); when: Expensive computations derived from state
-
name: "Immer for Immutable Updates" description: | Use Immer to write mutable-looking code that produces immutable updates. Built into Redux Toolkit, available as middleware for Zustand. example: | // Without Immer - verbose and error-prone const updateUser = (state, action) => ({ ...state, user: { ...state.user, profile: { ...state.user.profile, address: { ...state.user.profile.address, city: action.payload.city } } } });
// With Immer - write like mutations const updateUser = (state, action) => { state.user.profile.address.city = action.payload.city; };
// Zustand with Immer middleware import { immer } from 'zustand/middleware/immer';
const useStore = create( immer((set) => ({ users: [], addUser: (user) => set((state) => { state.users.push(user); }), })) ); when: Deeply nested state updates
-
name: "Persistence Middleware" description: | Persist state to localStorage/sessionStorage with automatic rehydration on app load. example: | // Zustand with persist import { persist } from 'zustand/middleware';
const useStore = create( persist( (set) => ({ theme: 'light', setTheme: (theme) => set({ theme }), }), { name: 'app-settings', partialize: (state) => ({ theme: state.theme }), // Only persist theme, not other state } ) );
// Redux with redux-persist import { persistStore, persistReducer } from 'redux-persist'; import storage from 'redux-persist/lib/storage';
const persistConfig = { key: 'root', storage, whitelist: ['settings', 'auth'], };
const persistedReducer = persistReducer(persistConfig, rootReducer); when: User preferences, auth tokens, draft content
-
name: "Optimistic Updates" description: | Update UI immediately before server confirms, then reconcile. Provides snappy UX while maintaining data integrity. example: | // Zustand optimistic update const useStore = create((set, get) => ({ todos: [],
addTodo: async (text) => { const optimisticTodo = { id: crypto.randomUUID(), text, completed: false, _optimistic: true, }; // Update immediately set((state) => ({ todos: [...state.todos, optimisticTodo] })); try { // Sync with server const realTodo = await api.createTodo(text); set((state) => ({ todos: state.todos.map(t => t.id === optimisticTodo.id ? realTodo : t ) })); } catch (error) { // Rollback on failure set((state) => ({ todos: state.todos.filter(t => t.id !== optimisticTodo.id) })); throw error; } },})); when: User actions that should feel instant
-
name: "Action Creators with Thunks" description: | Encapsulate async logic in action creators/thunks rather than components. Keeps components focused on UI. example: | // Redux Toolkit async thunk export const fetchUser = createAsyncThunk( 'user/fetch', async (userId: string, { rejectWithValue }) => { try { const response = await api.getUser(userId); return response.data; } catch (error) { return rejectWithValue(error.response.data); } } );
// Handles pending, fulfilled, rejected automatically const userSlice = createSlice({ name: 'user', initialState: { user: null, loading: false, error: null }, reducers: {}, extraReducers: (builder) => { builder .addCase(fetchUser.pending, (state) => { state.loading = true; }) .addCase(fetchUser.fulfilled, (state, action) => { state.user = action.payload; state.loading = false; }) .addCase(fetchUser.rejected, (state, action) => { state.error = action.payload; state.loading = false; }); }, }); when: Complex async operations with loading/error states
-
name: "Context for Dependency Injection" description: | Use React Context for dependency injection (services, config) rather than frequently-changing state. Context is for stable values. example: | // Good: Stable dependencies via Context const ApiContext = createContext<ApiClient | null>(null);
function ApiProvider({ children }) { const client = useMemo(() => new ApiClient(), []); return ( <ApiContext.Provider value={client}> {children} </ApiContext.Provider> ); }
// Bad: Frequently changing state via Context // This causes ALL consumers to re-render on every change const ThemeContext = createContext({ theme: 'light' });
// Better: Use Zustand/Jotai for frequently changing state const useTheme = create((set) => ({ theme: 'light', toggle: () => set(s => ({ theme: s.theme === 'light' ? 'dark' : 'light' })) })); when: Providing services/config that rarely change
============================================================================
ANTI-PATTERNS
============================================================================
anti_patterns:
-
name: "Storing Derived State" description: Storing computed values that can be derived from other state why: | Creates synchronization problems. If source state changes and derived state isn't updated, you have inconsistent state. instead: | Use selectors or computed values:
// Bad: Stored derived state const store = { items: [], itemCount: 0, // Derived from items.length totalPrice: 0, // Derived from items };
// Good: Compute on read const selectItemCount = (state) => state.items.length; const selectTotalPrice = createSelector( [selectItems], (items) => items.reduce((sum, i) => sum + i.price, 0) );
-
name: "Putting Everything in Global State" description: Moving all state to global store, including local UI state why: | Makes components less reusable, harder to test, and creates unnecessary coupling. Not all state needs to be global. instead: | Use the right level of state:
- Component state: useState for UI like open/closed, hover
- Lifted state: Parent component for sibling communication
- Context: Scoped global for feature areas
- Global store: Truly app-wide state (auth, theme, cart)
Decision tree
Is it used by multiple distant components? → Global store Is it used by nearby components? → Lift to common parent Is it only used here? → Local useState
-
name: "Mutating State Directly" description: Modifying state objects without creating new references why: | React and state libraries rely on reference equality to detect changes. Mutations are invisible, causing stale UI and bugs. instead: | Always create new references:
// Bad state.user.name = 'New Name'; setState(state);
// Good setState({ ...state, user: { ...state.user, name: 'New Name' } });
// Better: Use Immer setState(produce(state, draft => { draft.user.name = 'New Name'; }));
-
name: "Selector in Render" description: Creating selector functions inside render why: | Creates new function reference every render, breaking memoization and causing unnecessary re-renders. instead: | // Bad: New selector every render function UserList() { const users = useSelector(state => state.users.filter(u => u.active) ); }
// Good: Stable selector reference const selectActiveUsers = createSelector( [state => state.users], (users) => users.filter(u => u.active) );
function UserList() { const users = useSelector(selectActiveUsers); }
-
name: "Over-normalizing State" description: Normalizing all data into ID-lookup tables prematurely why: | Normalization adds complexity. Only normalize when you have actual problems with data duplication or update consistency. instead: | Start simple, normalize when needed:
// Start here { users: [{ id: 1, name: 'Alice' }] }
// Normalize if: // - Same entity appears in multiple places // - Updates need to reflect everywhere // - You have relational data (users + posts + comments)
// Normalized structure { users: { byId: { 1: {...} }, allIds: [1] }, posts: { byId: { ... }, allIds: [...] } }
-
name: "Redux for Simple Apps" description: Using Redux for small apps with minimal state why: | Redux has boilerplate overhead. For simple apps, the ceremony of actions, reducers, and types isn't worth it. instead: | Match tool to complexity:
- 1-5 pieces of global state → Zustand or Jotai
- Medium app with clear slices → Redux Toolkit or Zustand
- Large team, complex flows → Redux Toolkit with strict patterns
- Server state heavy → React Query + minimal client state
============================================================================
LIBRARY COMPARISON
============================================================================
library_comparison:
-
name: Zustand bundle_size: "~1.5KB" learning_curve: Low boilerplate: Minimal devtools: Yes (Redux DevTools) best_for: |
- Small to medium apps
- Teams wanting simplicity
- Quick prototypes
- Replacing Context + useReducer example: | const useStore = create((set) => ({ count: 0, increment: () => set((s) => ({ count: s.count + 1 })), }));
-
name: Redux Toolkit bundle_size: "~10KB" learning_curve: Medium boilerplate: Medium (reduced from classic Redux) devtools: Yes (excellent) best_for: |
- Large applications
- Teams with Redux experience
- Complex async flows
- Need for middleware ecosystem example: | const counterSlice = createSlice({ name: 'counter', initialState: { value: 0 }, reducers: { increment: (state) => { state.value += 1 }, }, });
-
name: Jotai bundle_size: "~2KB" learning_curve: Low boilerplate: Minimal devtools: Yes (Jotai DevTools) best_for: |
- Fine-grained reactivity needs
- Atomic state patterns
- React Suspense integration
- Avoiding re-render cascades example: | const countAtom = atom(0); const doubleAtom = atom((get) => get(countAtom) * 2);
-
name: MobX bundle_size: "~15KB" learning_curve: Medium boilerplate: Low devtools: Yes (MobX DevTools) best_for: |
- OOP-style state management
- Automatic dependency tracking
- Teams familiar with observables
- Complex derived state example: | class TodoStore { todos = []; get completedCount() { return this.todos.filter(t => t.done).length; } }
-
name: Recoil bundle_size: "~20KB" learning_curve: Medium boilerplate: Low devtools: Recoil DevTools best_for: |
- Facebook-scale apps
- Concurrent mode ready
- Complex derived state graphs
- React-first teams note: "Development has slowed; consider Jotai as alternative"
============================================================================
HANDOFFS
============================================================================
handoffs: receives_from: - skill: frontend context: "Frontend needs state management setup" receives: - Component structure - Data flow requirements - Performance constraints provides: "State management architecture and implementation"
- skill: react-patterns context: "React patterns need state solution" receives: - Component hierarchy - State requirements - Re-render concerns provides: "Optimized state management integration"
hands_to: - skill: performance-optimization trigger: "State causing performance issues" provides: - Current state structure - Re-render patterns - Selector usage receives: "Optimization recommendations"
- skill: testing-automation trigger: "Need to test stateful components" provides: - Store structure - Action types - Mock strategies receives: "Test patterns for state"