Back to React & React Native

react-state-management

reactstate managementreduxzustandjotaireact queryfrontendjavascript
⭐ 36.8kπŸ“„ MITπŸ•’ 2026-06-16Source β†—

Install this skill

npx skills add wshobson/agents

Works across Claude Code, Cursor, Codex, Copilot & Antigravity

React state management involves choosing appropriate storage patterns to handle data lifecycle and UI synchronization. Applications require different approaches based on the scope and origin of information. Local UI concerns remain best served by built-in hooks, while cross-component data needs dedicated stores like Zustand or Redux Toolkit. Server-side data requires asynchronous caching solutions to minimize network traffic and keep the UI consistent. URL parameters and form input values represent specialized categories that benefit from distinct libraries like nuqs or React Hook Form. By distinguishing between volatile UI state, persistent global stores, and cacheable server results, developers prevent unnecessary re-renders and logic bloat. Effective architecture relies on matching the right strategy to the specific data ownership requirements of the component tree.

When to Use This Skill

  • β€’Sharing user authentication sessions across the entire application
  • β€’Caching API response data to prevent redundant network requests
  • β€’Managing complex form validation states with dependencies
  • β€’Synchronizing UI state with browser URL query parameters
  • β€’Maintaining dark mode or theme preference settings

How to Invoke This Skill

Example prompts that trigger this skill in Claude Code, Cursor, or Antigravity:

  • β€œHow should I share data between these components?
  • β€œSet up a global store using Zustand
  • β€œHow to manage server API cache in React?
  • β€œChoosing between Redux and React Query
  • β€œImplement persistent state in my React app

Pro Tips

  • πŸ’‘Always co-locate state as close as possible to where it's consumed before lifting it to a global store.
  • πŸ’‘For server state, prioritize React Query/SWR for most data fetching, reserving Redux for true global client-side state.
  • πŸ’‘When using Redux Toolkit, embrace Immer for immutable updates and `createSlice` for boilerplate reduction.

What this skill does

  • β€’Defining atomic or centralized global data stores
  • β€’Implementing asynchronous server data synchronization and caching
  • β€’Reducing prop-drilling by sharing state across disconnected component trees
  • β€’Handling complex side effects through middleware or reducer logic
  • β€’Persistent storage of store values in browser memory
  • β€’Optimistic UI updates for high-latency server interactions

When not to use it

  • βœ•When simple component-level props pass effectively without creating prop-drilling
  • βœ•When the data is strictly local and does not need to be accessed by siblings or ancestors

Example workflow

  1. Identify if data is UI-local, server-side, or global
  2. Install the appropriate library based on application size
  3. Define the initial state schema and update functions
  4. Create hooks to interface with the state layer
  5. Integrate state selectors into target functional components

Prerequisites

  • –Fundamental understanding of React Hooks
  • –Basic knowledge of TypeScript interfaces
  • –Familiarity with functional programming concepts

Pitfalls & limitations

  • !Over-engineering simple apps by introducing heavy state libraries
  • !Storing server-side data in global stores instead of dedicated cache managers
  • !Causing unnecessary re-renders by selecting too much state in components

FAQ

Should I use Redux for every project?
No, Redux is often overkill for smaller applications. Consider lightweight alternatives like Zustand or Jotai unless your project requires massive state complexity and strict middleware.
Is React Query a state manager?
It manages server state specifically. It excels at caching and synchronization but should be paired with a store like Zustand if you have complex local UI state.
Can I use multiple state libraries?
Yes. It is common to use React Query for API data and Zustand for local configuration settings within the same application.

How it compares

Generic prompts often suggest using Context API for everything, which ignores the performance costs and re-render issues that specialized libraries like Zustand handle via optimized selectors.

Source & trust

⭐ 37k starsπŸ“„ MITπŸ•’ Updated 2026-06-16
πŸ“„ Full skill instructions β€” original source: wshobson/agents
# 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>
)
}


## Patterns

### Pattern 1: Redux Toolkit with 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;


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

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

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

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

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

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

How to Use This Skill Unit

Option A: Project-Specific (Recommended)

  1. Click "Download" above
  2. In your project, create the directory: .agent/skills/react-state-management/
  3. Save the file as SKILL.md
  4. The agent will automatically discover the skill based on its description.

Option B: Global Installation (All Agents)

Save the file to these locations to make it available across all projects:

  • Claude Code: ~/.claude/skills/wshobson/agents/react-state-management/SKILL.md
  • Cursor: ~/.cursor/skills/wshobson/agents/react-state-management/SKILL.md
  • Antigravity: ~/.gemini/antigravity/skills/wshobson/agents/react-state-management/SKILL.md

πŸš€ Install with CLI:
npx skills add wshobson/agents

Read the Master Guide: Mastering Agent Skills β†’

Recommended Rules

View more rules β†’

Recommended Workflows

View more workflows β†’

Recommended MCP Servers

View more MCP servers β†’

Take It Further

Maximize your productivity with these powerful resources

πŸ“‹

Define Your Standards

Set up coding standards to ensure this workflow produces consistent, high-quality results.

Browse Rules Library
πŸ“–

Master Workflows

Learn how to create custom workflows, use Turbo Mode, and build your automation library.

Complete Guide

How to use this Skill in Claude Code & Cursor

For Claude Code (CLI)

To use this skill in Claude Code, copy the rule content into your project's custom instructions or follow our Add-Skill CLI guide. This ensures Claude follows your standards during every code generation.

For Cursor & Windsurf

For Cursor or Windsurf, individual skills are best used in the "Rules for AI" section. This specific unit helps the agent avoid react & react native issues, leading to cleaner, more efficient code.

Why the skill format matters: the standardized Agent Skills format lets your AI agent load detailed instructions only when they are relevant, keeping your prompt clean while improving results.

Source & attribution

This skill is categorized under React & React Native and is published by W. Shobson, maintained in wshobson/agents.

← Browse All Agent Skills
Sponsored AI assistant. Recommendations may be paid.