state-management
State Management
Choose the right state management approach for your React application.
Instructions
- Start simple - Use useState/useReducer before reaching for libraries
- Separate concerns - UI state vs server state vs global state
- Collocate state - Keep state close to where it's used
- Use the right tool - Different state types need different solutions
- Avoid over-engineering - Not everything needs global state
State Types
| Type | Examples | Solution |
|---|---|---|
| Local UI | Form inputs, toggles, modals | useState, useReducer |
| Server | API data, cached responses | React Query, SWR |
| Global UI | Theme, sidebar state, user preferences | Zustand, Jotai |
| Complex Global | Shopping cart, multi-step forms | Zustand, Redux Toolkit |
Zustand (Recommended)
Basic Store
import { create } from 'zustand';
interface CounterStore {
count: number;
increment: () => void;
decrement: () => void;
reset: () => void;
}
const useCounterStore = create<CounterStore>((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
}));
// Usage
function Counter() {
const { count, increment, decrement } = useCounterStore();
return (
<div>
<span>{count}</span>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
);
}
Async Actions
interface UserStore {
user: User | null;
loading: boolean;
error: string | null;
fetchUser: (id: string) => Promise<void>;
logout: () => void;
}
const useUserStore = create<UserStore>((set) => ({
user: null,
loading: false,
error: null,
fetchUser: async (id) => {
set({ loading: true, error: null });
try {
const response = await fetch(`/api/users/${id}`);
const user = await response.json();
set({ user, loading: false });
} catch (error) {
set({ error: 'Failed to fetch user', loading: false });
}
},
logout: () => set({ user: null }),
}));
Selectors for Performance
// Only re-render when specific value changes
function UserName() {
const name = useUserStore((state) => state.user?.name);
return <span>{name}</span>;
}
// Multiple values with shallow compare
import { shallow } from 'zustand/shallow';
function UserInfo() {
const { name, email } = useUserStore(
(state) => ({ name: state.user?.name, email: state.user?.email }),
shallow
);
return <div>{name} - {email}</div>;
}
Persistence
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
const useSettingsStore = create(
persist<SettingsStore>(
(set) => ({
theme: 'light',
language: 'en',
setTheme: (theme) => set({ theme }),
setLanguage: (language) => set({ language }),
}),
{
name: 'settings-storage',
partialize: (state) => ({ theme: state.theme, language: state.language }),
}
)
);
Slices Pattern
// auth-slice.ts
export const createAuthSlice = (set) => ({
user: null,
isAuthenticated: false,
login: (user) => set({ user, isAuthenticated: true }),
logout: () => set({ user: null, isAuthenticated: false }),
});
// cart-slice.ts
export const createCartSlice = (set) => ({
items: [],
addItem: (item) => set((state) => ({ items: [...state.items, item] })),
removeItem: (id) => set((state) => ({
items: state.items.filter((i) => i.id !== id),
})),
});
// store.ts
const useStore = create((...args) => ({
...createAuthSlice(...args),
...createCartSlice(...args),
}));
React Query (Server State)
Basic Query
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
function UserProfile({ userId }) {
const { data: user, isLoading, error } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
staleTime: 5 * 60 * 1000, // 5 minutes
});
if (isLoading) return <Spinner />;
if (error) return <Error message={error.message} />;
return <Profile user={user} />;
}
Mutations with Optimistic Updates
function TodoList() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: updateTodo,
onMutate: async (newTodo) => {
await queryClient.cancelQueries({ queryKey: ['todos'] });
const previousTodos = queryClient.getQueryData(['todos']);
queryClient.setQueryData(['todos'], (old) =>
old.map((todo) => (todo.id === newTodo.id ? newTodo : todo))
);
return { previousTodos };
},
onError: (err, newTodo, context) => {
queryClient.setQueryData(['todos'], context.previousTodos);
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => mutation.mutate({ ...todo, completed: !todo.completed })}
/>
{todo.title}
</li>
))}
</ul>
);
}
Infinite Queries
function InfiniteList() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfiniteQuery({
queryKey: ['posts'],
queryFn: ({ pageParam = 0 }) => fetchPosts(pageParam),
getNextPageParam: (lastPage) => lastPage.nextCursor,
});
return (
<div>
{data?.pages.map((page) =>
page.posts.map((post) => <PostCard key={post.id} post={post} />)
)}
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage ? 'Loading...' : hasNextPage ? 'Load More' : 'No more posts'}
</button>
</div>
);
}
Jotai (Atomic State)
Basic Atoms
import { atom, useAtom } from 'jotai';
const countAtom = atom(0);
const doubleCountAtom = atom((get) => get(countAtom) * 2);
function Counter() {
const [count, setCount] = useAtom(countAtom);
const [doubleCount] = useAtom(doubleCountAtom);
return (
<div>
<p>Count: {count}</p>
<p>Double: {doubleCount}</p>
<button onClick={() => setCount((c) => c + 1)}>Increment</button>
</div>
);
}
Async Atoms
const userIdAtom = atom(1);
const userAtom = atom(async (get) => {
const id = get(userIdAtom);
const response = await fetch(`/api/users/${id}`);
return response.json();
});
function User() {
const [user] = useAtom(userAtom);
return <div>{user.name}</div>;
}
Redux Toolkit (Complex State)
Store Setup
import { configureStore, createSlice, PayloadAction } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: (state) => { state.value += 1; },
decrement: (state) => { state.value -= 1; },
incrementByAmount: (state, action: PayloadAction<number>) => {
state.value += action.payload;
},
},
});
const store = configureStore({
reducer: {
counter: counterSlice.reducer,
},
});
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export type RootState = ReturnType<typeof store.getState>;
Async Thunks
import { createAsyncThunk } from '@reduxjs/toolkit';
export const fetchUser = createAsyncThunk(
'users/fetchById',
async (userId: string) => {
const response = await fetch(`/api/users/${userId}`);
return response.json();
}
);
const usersSlice = createSlice({
name: 'users',
initialState: { user: null, loading: false, error: null },
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchUser.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(fetchUser.fulfilled, (state, action) => {
state.loading = false;
state.user = action.payload;
})
.addCase(fetchUser.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message ?? 'Failed';
});
},
});
Decision Guide
Need to manage state?
|
v
Is it server data (API responses)?
Yes -> React Query or SWR
|
No
|
v
Is it used by only one component?
Yes -> useState or useReducer
|
No
|
v
Is it used by a few nearby components?
Yes -> Lift state up or Context
|
No
|
v
Is it complex with many actions?
Yes -> Redux Toolkit
|
No -> Zustand or Jotai
When to Use
- useState: Simple local state, form inputs
- useReducer: Complex local state with multiple sub-values
- Context: Theme, auth status (rarely changing data)
- Zustand: Global UI state, shopping carts, preferences
- React Query: All server/API data
- Redux Toolkit: Very complex state, time-travel debugging needs
- Jotai: Fine-grained reactivity, many independent atoms
Notes
- Don't put everything in global state
- Server state and UI state are different concerns
- Zustand is simpler than Redux for most use cases
- React Query eliminates most "loading/error/data" boilerplate
- Test your state management logic separately from components
More from housegarofalo/claude-code-base
mqtt-iot
Configure MQTT brokers (Mosquitto, EMQX) for IoT messaging, device communication, and smart home integration. Manage topics, QoS levels, authentication, and bridging. Use when setting up IoT messaging, smart home communication, or device-to-cloud connectivity. (project)
22home-assistant
Ultimate Home Assistant skill - complete administration, wireless protocols (Zigbee/ZHA/Z2M, Z-Wave JS, Thread, Matter), ESPHome device building, advanced troubleshooting, performance optimization, security hardening, custom integration development, and professional dashboard design. Covers configuration, REST API, automation debugging, database optimization, SSL/TLS, Jinja2 templating, and HACS custom cards. Use for any HA task.
6testing
Comprehensive testing skill covering unit, integration, and E2E testing with pytest, Jest, Cypress, and Playwright. Use for writing tests, improving coverage, debugging test failures, and setting up testing infrastructure.
5react-typescript
Build modern React applications with TypeScript. Covers React 18+ patterns, hooks, component architecture, state management (Zustand, Redux Toolkit), server components, and best practices. Use for React development, TypeScript integration, component design, and frontend architecture.
5power-automate
Expert guidance for Power Automate development including cloud flows, desktop flows, Dataverse connector, expression functions, custom connectors, error handling, and child flow patterns. Use when building automated workflows, writing flow expressions, creating custom connectors from OpenAPI, or implementing error handling patterns.
5mobile-pwa
Build Progressive Web Apps with offline support, push notifications, and native-like experiences. Covers service workers, Web App Manifest, caching strategies, IndexedDB, background sync, and installability. Use for mobile-first web apps, offline-capable applications, and app-like experiences.
5