state-management
State management patterns - Zustand, Jotai, Context
$ Installer
git clone https://github.com/emanueleielo/deepagents-open-lovable /tmp/deepagents-open-lovable && cp -r /tmp/deepagents-open-lovable/agent/skills/state-management ~/.claude/skills/deepagents-open-lovable// tip: Run this command in your terminal to install the skill
SKILL.md
name: state-management description: State management patterns - Zustand, Jotai, Context
State Management
When to Use What
| State Type | Solution | Example |
|---|---|---|
| Server state | TanStack Query | API data, user profile |
| Form state | React Hook Form | Form inputs, validation |
| Local UI | useState | Modal open, input value |
| Shared UI | Zustand / Jotai | Theme, sidebar open, filters |
| Complex shared | Zustand | Shopping cart, multi-step wizard |
Rule: Server data belongs in TanStack Query, NOT in global state.
1. Zustand - Simple Global State
import { create } from "zustand";
import { persist } from "zustand/middleware";
// Basic store
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>
);
}
// Select specific values (prevents re-renders)
function CountDisplay() {
const count = useCounterStore((state) => state.count);
return <span>{count}</span>;
}
2. Zustand - Complex Store with Slices
import { create, StateCreator } from "zustand";
import { devtools, persist } from "zustand/middleware";
// User slice
interface UserSlice {
user: User | null;
setUser: (user: User | null) => void;
}
const createUserSlice: StateCreator<UserSlice> = (set) => ({
user: null,
setUser: (user) => set({ user }),
});
// Cart slice
interface CartSlice {
items: CartItem[];
addItem: (item: CartItem) => void;
removeItem: (id: string) => void;
clearCart: () => void;
total: () => number;
}
const createCartSlice: StateCreator<CartSlice & UserSlice, [], [], CartSlice> = (set, get) => ({
items: [],
addItem: (item) => set((state) => ({
items: [...state.items, item]
})),
removeItem: (id) => set((state) => ({
items: state.items.filter((i) => i.id !== id)
})),
clearCart: () => set({ items: [] }),
total: () => get().items.reduce((sum, item) => sum + item.price, 0),
});
// Combined store
type Store = UserSlice & CartSlice;
const useStore = create<Store>()(
devtools(
persist(
(...a) => ({
...createUserSlice(...a),
...createCartSlice(...a),
}),
{ name: "app-store" }
)
)
);
3. Jotai - Atomic State
import { atom, useAtom, useAtomValue, useSetAtom } from "jotai";
import { atomWithStorage } from "jotai/utils";
// Primitive atom
const countAtom = atom(0);
// Derived atom (read-only)
const doubleAtom = atom((get) => get(countAtom) * 2);
// Writable derived atom
const countWithMaxAtom = atom(
(get) => get(countAtom),
(get, set, newValue: number) => {
set(countAtom, Math.min(newValue, 100));
}
);
// Async atom
const userAtom = atom(async () => {
const res = await fetch("/api/user");
return res.json();
});
// Persisted atom
const themeAtom = atomWithStorage<"light" | "dark">("theme", "light");
// Usage
function Counter() {
const [count, setCount] = useAtom(countAtom);
const double = useAtomValue(doubleAtom);
return (
<div>
<span>{count} (double: {double})</span>
<button onClick={() => setCount((c) => c + 1)}>+</button>
</div>
);
}
// Only set (no re-render on value change)
function IncrementButton() {
const setCount = useSetAtom(countAtom);
return <button onClick={() => setCount((c) => c + 1)}>+</button>;
}
4. Jotai - Atom Families
import { atom } from "jotai";
import { atomFamily } from "jotai/utils";
// Atom family for per-item state
const itemQuantityAtomFamily = atomFamily((itemId: string) =>
atom(1)
);
// Usage
function ItemQuantity({ itemId }: { itemId: string }) {
const [quantity, setQuantity] = useAtom(itemQuantityAtomFamily(itemId));
return (
<div>
<button onClick={() => setQuantity((q) => Math.max(1, q - 1))}>-</button>
<span>{quantity}</span>
<button onClick={() => setQuantity((q) => q + 1)}>+</button>
</div>
);
}
5. Context API - When Appropriate
Use Context for:
- Dependency injection (services, config)
- Compound components (sharing state between parent/children)
- Theme/i18n that rarely changes
import { createContext, useContext, useState, ReactNode } from "react";
interface ThemeContextValue {
theme: "light" | "dark";
toggle: () => void;
}
const ThemeContext = createContext<ThemeContextValue | null>(null);
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) throw new Error("useTheme must be used within ThemeProvider");
return context;
}
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState<"light" | "dark">("light");
const toggle = () => setTheme((t) => (t === "light" ? "dark" : "light"));
return (
<ThemeContext.Provider value={{ theme, toggle }}>
{children}
</ThemeContext.Provider>
);
}
6. Combining with TanStack Query
// Store for UI state only
const useUIStore = create<UIStore>((set) => ({
sidebarOpen: true,
toggleSidebar: () => set((s) => ({ sidebarOpen: !s.sidebarOpen })),
// Filters affect query key
filters: { status: "all", search: "" },
setFilters: (filters) => set({ filters }),
}));
// Component
function UserList() {
const filters = useUIStore((s) => s.filters);
// Server state in Query, UI filters in Zustand
const { data: users } = useQuery({
queryKey: ["users", filters],
queryFn: () => fetchUsers(filters),
});
return (
<div>
<FilterBar />
{users?.map((user) => <UserCard key={user.id} user={user} />)}
</div>
);
}
Comparison
| Feature | Zustand | Jotai | Context |
|---|---|---|---|
| Boilerplate | Low | Very low | Medium |
| DevTools | Yes | Yes | React DevTools |
| Persistence | Middleware | Built-in | Manual |
| Selectors | Built-in | Atoms | useMemo |
| Async | Manual | Built-in | Manual |
| Best for | Single store | Many atoms | DI, compound |
Best Practices
- Don't put server data in global state - use TanStack Query
- Use selectors to prevent unnecessary re-renders
- Keep stores small and focused
- Colocate state - prefer local state when possible
- Use devtools in development
- Persist only necessary data (user preferences, cart)
Repository

emanueleielo
Author
emanueleielo/deepagents-open-lovable/agent/skills/state-management
2
Stars
0
Forks
Updated2d ago
Added1w ago