From react-toolkit
Advises on selecting and implementing React state management: useState, Context, Zustand, Redux Toolkit, TanStack Query, Jotai for local, global, and server state.
How this skill is triggered — by the user, by Claude, or both
Slash command
/react-toolkit:state-management-advisorThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Choose and implement the right state management solution for React applications.
Choose and implement the right state management solution for React applications.
Use local state by default, Context for simple global state, Zustand for complex client state, and TanStack Query for server state.
Start here:
For component-specific state.
Basic usage:
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}
With objects:
function Form() {
const [formData, setFormData] = useState({
name: '',
email: ''
});
const handleChange = (field: string, value: string) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
return (
<form>
<input
value={formData.name}
onChange={e => handleChange('name', e.target.value)}
/>
</form>
);
}
For simple global state shared across components.
Setup:
interface AuthContextValue {
user: User | null;
login: (credentials: Credentials) => Promise<void>;
logout: () => void;
}
const AuthContext = createContext<AuthContextValue | null>(null);
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const login = async (credentials: Credentials) => {
const user = await api.login(credentials);
setUser(user);
};
const logout = () => {
setUser(null);
};
return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) throw new Error('useAuth must be used within AuthProvider');
return context;
}
Usage:
function App() {
return (
<AuthProvider>
<Router />
</AuthProvider>
);
}
function Profile() {
const { user, logout } = useAuth();
return <div>{user?.name} <button onClick={logout}>Logout</button></div>;
}
Optimization - Split contexts:
// Separate frequently changing data
const ThemeContext = createContext<Theme>(null);
const ThemeUpdateContext = createContext<(theme: Theme) => void>(null);
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={theme}>
<ThemeUpdateContext.Provider value={setTheme}>
{children}
</ThemeUpdateContext.Provider>
</ThemeContext.Provider>
);
}
Lightweight global state with minimal boilerplate.
Installation:
npm install zustand
Basic store:
import { create } from 'zustand';
interface CounterStore {
count: number;
increment: () => void;
decrement: () => void;
}
const useCounterStore = create<CounterStore>((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
}));
// Usage
function Counter() {
const count = useCounterStore((state) => state.count);
const increment = useCounterStore((state) => state.increment);
return <button onClick={increment}>Count: {count}</button>;
}
With async actions:
interface UserStore {
users: User[];
loading: boolean;
fetchUsers: () => Promise<void>;
}
const useUserStore = create<UserStore>((set) => ({
users: [],
loading: false,
fetchUsers: async () => {
set({ loading: true });
const users = await api.fetchUsers();
set({ users, loading: false });
},
}));
With middleware:
import { persist } from 'zustand/middleware';
const useStore = create(
persist(
(set) => ({
token: null,
setToken: (token) => set({ token }),
}),
{
name: 'auth-storage',
}
)
);
For complex state with many interactions.
Installation:
npm install @reduxjs/toolkit react-redux
Setup store:
import { configureStore, createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: (state) => {
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
},
});
export const { increment, decrement } = counterSlice.actions;
export const store = configureStore({
reducer: {
counter: counterSlice.reducer,
},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
Provider:
import { Provider } from 'react-redux';
function App() {
return (
<Provider store={store}>
<Router />
</Provider>
);
}
Usage:
import { useSelector, useDispatch } from 'react-redux';
function Counter() {
const count = useSelector((state: RootState) => state.counter.value);
const dispatch = useDispatch();
return (
<button onClick={() => dispatch(increment())}>
Count: {count}
</button>
);
}
Async with createAsyncThunk:
import { createAsyncThunk } from '@reduxjs/toolkit';
export const fetchUsers = createAsyncThunk(
'users/fetch',
async () => {
const response = await api.fetchUsers();
return response.data;
}
);
const usersSlice = createSlice({
name: 'users',
initialState: { data: [], loading: false },
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchUsers.pending, (state) => {
state.loading = true;
})
.addCase(fetchUsers.fulfilled, (state, action) => {
state.data = action.payload;
state.loading = false;
});
},
});
For server state management.
Installation:
npm install @tanstack/react-query
Setup:
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient();
function App() {
return (
<QueryClientProvider client={queryClient}>
<Router />
</QueryClientProvider>
);
}
Fetching data:
import { useQuery } from '@tanstack/react-query';
function Users() {
const { data, isLoading, error } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
});
if (isLoading) return <Spinner />;
if (error) return <Error message={error.message} />;
return <UserList users={data} />;
}
Mutations:
import { useMutation, useQueryClient } from '@tanstack/react-query';
function CreateUser() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: createUser,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
},
});
return (
<button onClick={() => mutation.mutate({ name: 'John' })}>
Create User
</button>
);
}
With parameters:
function User({ id }: { id: string }) {
const { data } = useQuery({
queryKey: ['user', id],
queryFn: () => fetchUser(id),
});
return <div>{data?.name}</div>;
}
Optimistic updates:
const mutation = useMutation({
mutationFn: updateUser,
onMutate: async (newUser) => {
await queryClient.cancelQueries({ queryKey: ['users'] });
const previousUsers = queryClient.getQueryData(['users']);
queryClient.setQueryData(['users'], (old) => [...old, newUser]);
return { previousUsers };
},
onError: (err, newUser, context) => {
queryClient.setQueryData(['users'], context.previousUsers);
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
},
});
Atomic state management.
Installation:
npm install jotai
Basic atoms:
import { atom, useAtom } from 'jotai';
const countAtom = atom(0);
function Counter() {
const [count, setCount] = useAtom(countAtom);
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}
Derived atoms:
const countAtom = atom(0);
const doubleCountAtom = atom((get) => get(countAtom) * 2);
function Display() {
const [doubleCount] = useAtom(doubleCountAtom);
return <div>Double: {doubleCount}</div>;
}
Async atoms:
const usersAtom = atom(async () => {
const response = await fetch('/api/users');
return response.json();
});
function Users() {
const [users] = useAtom(usersAtom);
return <UserList users={users} />;
}
| Solution | Best For | Pros | Cons |
|---|---|---|---|
| useState | Local state | Simple, built-in | Limited to component |
| Context | Simple global state | Built-in, no deps | Can cause re-renders |
| Zustand | Complex client state | Minimal, fast | Smaller ecosystem |
| Redux Toolkit | Large apps, teams | Powerful, DevTools | Verbose, learning curve |
| TanStack Query | Server state | Caching, sync | Not for client state |
| Jotai | Atomic state | Fine-grained, modern | Smaller ecosystem |
TanStack Query + Zustand:
// Server state with TanStack Query
const { data: users } = useQuery(['users'], fetchUsers);
// Client state with Zustand
const selectedUserId = useStore((state) => state.selectedUserId);
Context + TanStack Query:
function AuthProvider({ children }) {
const { data: user } = useQuery(['me'], fetchCurrentUser);
return (
<AuthContext.Provider value={{ user }}>
{children}
</AuthContext.Provider>
);
}
Controlled with useState:
function Form() {
const [values, setValues] = useState({ name: '', email: '' });
const handleSubmit = (e) => {
e.preventDefault();
api.submit(values);
};
return (
<form onSubmit={handleSubmit}>
<input
value={values.name}
onChange={e => setValues(prev => ({ ...prev, name: e.target.value }))}
/>
</form>
);
}
With form library:
import { useForm } from 'react-hook-form';
function Form() {
const { register, handleSubmit } = useForm();
const onSubmit = (data) => api.submit(data);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('name')} />
</form>
);
}
Context causing re-renders:
Stale data in TanStack Query:
Redux boilerplate:
State updates not reflecting:
npx claudepluginhub p/armanzeroeight-react-toolkit-plugins-react-toolkitGuides React state management patterns with Redux Toolkit, Zustand, Jotai, React Query for local, global, server state, and library selection.
Guides frontend state management in React: local/global decisions, Zustand/Redux Toolkit/Jotai/MobX/Context, TanStack Query/SWR for server state, optimistic updates, XState machines. Use for store setup, migrations, re-render fixes.
Guides React state management with Redux Toolkit, Zustand, Jotai, and React Query. Covers global state, server state, optimistic updates, and migration from legacy Redux.