Claude Cursor Skill

react-state-management

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.

LLM Mart · 0 points · 16 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download wshobson-agents-plugins_frontend-mobile-development_skills_react-state-management-554237f.zip · 4 KB
Part of wshobson/agents — 170 skills

Install

skills CLI npx skills add https://github.com/wshobson/agents/tree/main/plugins/frontend-mobile-development/skills/react-state-management
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install wshobson-agents@llmmart
Git git clone https://github.com/wshobson/agents.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole wshobson/agents collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

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)

// 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>
  )
}

Detailed patterns and worked examples

Detailed pattern documentation lives in references/details.md. Read that file when the navigation tier above is insufficient.

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

// 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 });
    },
  },
});
Files (agents)
  • references
    • details.md 7.6 KB
      # react-state-management — detailed patterns and worked examples
      
      ## 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>
        )
      }
      ```
      
  • SKILL.md 3.9 KB
    ---
    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>
      )
    }
    ```
    
    ## Detailed patterns and worked examples
    
    Detailed pattern documentation lives in `references/details.md`. Read that file when the navigation tier above is insufficient.
    
    ## 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 });
        },
      },
    });
    ```
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related