Claude Agent Skill · by Wshobson

React State Management

Solid reference for navigating React's state management ecosystem. Covers the decision matrix between Redux Toolkit, Zustand, and Jotai with practical TypeScrip

Install
Terminal · npx
$npx skills add https://github.com/wshobson/agents --skill react-state-management
Works with Paperclip

How React State Management fits into a Paperclip company.

React State Management drops into any Paperclip agent that handles this kind of work. Assign it to a specialist inside a pre-configured PaperclipOrg company and the skill becomes available on every heartbeat — no prompt engineering, no tool wiring.

S
SaaS FactoryPaired

Pre-configured AI company — 18 agents, 18 skills, one-time purchase.

$27$59
Explore pack
Source file
SKILL.md430 lines
Expand
---name: react-state-managementdescription: 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 JotaiLarge app, complex state → Redux ToolkitHeavy server interaction → React Query + light client stateAtomic/granular updates → Jotai``` ## Quick Start ### Zustand (Simplest) ```typescript// store/useStore.tsimport { 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 componentfunction 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.tsimport { 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 hooksexport const useAppDispatch: () => AppDispatch = useDispatch;export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;``` ```typescript// store/slices/userSlice.tsimport { 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.tsimport { 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.tsimport { 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.tsimport { atom } from 'jotai'import { atomWithStorage } from 'jotai/utils' // Basic atomexport const userAtom = atom<User | null>(null) // Derived atom (computed)export const isAuthenticatedAtom = atom((get) => get(userAtom) !== null) // Atom with localStorage persistenceexport const themeAtom = atomWithStorage<'light' | 'dark'>('theme', 'light') // Async atomexport 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')}) // Usagefunction 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.tsimport { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; // Query keys factoryexport 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 hookexport 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 hookexport function useUser(id: string) {  return useQuery({    queryKey: userKeys.detail(id),    queryFn: () => fetchUser(id),    enabled: !!id, // Don't fetch if no id  });} // Mutation with optimistic updateexport 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 stateconst 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 statefunction 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 });    },  },});```