skills/_templates/tech/zustand-5/SKILL.md
Zustand 5 state management patterns for React: stores, persistence, selectors, slices. Trigger: When managing global state in React, using Zustand, or implementing state slices.
npx skillsauth add fearovex/claude-config zustand-5Install this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Triggers: When managing global state in React, using Zustand, or implementing state slices.
Load when: managing global or shared state in React, implementing persistence, using Zustand stores, or needing out-of-component state access.
import { create } from 'zustand';
interface CounterState {
count: number;
increment: () => void;
decrement: () => void;
reset: () => void;
}
export const useCounterStore = create<CounterState>()((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
}));
// ❌ Selects the entire store — re-renders on ANY change
const store = useCounterStore();
// ✅ Select only what you need
const count = useCounterStore((state) => state.count);
const increment = useCounterStore((state) => state.increment);
// ✅ Multiple values with useShallow
import { useShallow } from 'zustand/react/shallow';
const { count, increment } = useCounterStore(
useShallow((state) => ({ count: state.count, increment: state.increment }))
);
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
interface SettingsState {
theme: 'light' | 'dark';
language: string;
setTheme: (theme: 'light' | 'dark') => void;
setLanguage: (lang: string) => void;
}
export const useSettingsStore = create<SettingsState>()(
persist(
(set) => ({
theme: 'light',
language: 'es',
setTheme: (theme) => set({ theme }),
setLanguage: (language) => set({ language }),
}),
{ name: 'settings-storage' } // localStorage key
)
);
interface UserState {
users: User[];
isLoading: boolean;
error: string | null;
fetchUsers: () => Promise<void>;
createUser: (data: CreateUserInput) => Promise<void>;
}
export const useUserStore = create<UserState>()((set) => ({
users: [],
isLoading: false,
error: null,
fetchUsers: async () => {
set({ isLoading: true, error: null });
try {
const users = await api.getUsers();
set({ users, isLoading: false });
} catch (error) {
set({ error: String(error), isLoading: false });
}
},
createUser: async (data) => {
set({ isLoading: true });
try {
const user = await api.createUser(data);
set((state) => ({ users: [...state.users, user], isLoading: false }));
} catch (error) {
set({ error: String(error), isLoading: false });
}
},
}));
// userSlice.ts
interface UserSlice {
user: User | null;
setUser: (user: User) => void;
clearUser: () => void;
}
const createUserSlice = (set: any): UserSlice => ({
user: null,
setUser: (user) => set({ user }),
clearUser: () => set({ user: null }),
});
// cartSlice.ts
interface CartSlice {
items: CartItem[];
addItem: (item: CartItem) => void;
removeItem: (id: string) => void;
}
const createCartSlice = (set: any): CartSlice => ({
items: [],
addItem: (item) => set((state: any) => ({ items: [...state.items, item] })),
removeItem: (id) => set((state: any) => ({
items: state.items.filter((i: CartItem) => i.id !== id)
})),
});
// store.ts — combine slices
export const useStore = create<UserSlice & CartSlice>()((...args) => ({
...createUserSlice(...args),
...createCartSlice(...args),
}));
import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';
interface TodoState {
todos: { id: string; text: string; done: boolean }[];
toggleTodo: (id: string) => void;
}
export const useTodoStore = create<TodoState>()(
immer((set) => ({
todos: [],
toggleTodo: (id) => set((state) => {
const todo = state.todos.find((t) => t.id === id);
if (todo) todo.done = !todo.done; // Direct mutation OK with Immer
}),
}))
);
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
export const useAppStore = create<AppState>()(
devtools(
(set) => ({ /* ... */ }),
{ name: 'AppStore' } // Name in Redux DevTools
)
);
// Access outside of components
const state = useAppStore.getState();
const unsubscribe = useAppStore.subscribe(
(state) => state.user,
(user) => console.log('User changed:', user)
);
// ❌ Re-renders on any store change
const { count, user, settings, items } = useStore();
// ✅ Select only what you need
const count = useStore((s) => s.count);
// ❌ Don't put async inside set
set(async (state) => { /* ... */ });
// ✅ Use get() or define the async in the action
fetchData: async () => {
const data = await api.get();
set({ data });
}
| Task | Pattern |
|------|---------|
| Basic store | create<State>()((set) => ...) |
| Simple selector | useStore((s) => s.field) |
| Multiple fields | useStore(useShallow((s) => ({a: s.a, b: s.b}))) |
| Persistence | create()(persist(..., { name: 'key' })) |
| Mutations | create()(immer(...)) |
| DevTools | create()(devtools(..., { name: 'Name' })) |
| External access | useStore.getState() |
| Subscription | useStore.subscribe(selector, callback) |
useStore(s => s.count)) — subscribing to the full store object causes unnecessary re-renders on any state changezustand/middleware) must be applied only to stores that genuinely need persistence; over-persisting creates stale-state bugs after schema changescreate callback, not as external functions that receive the store as a parameteruseShallow for object selectors to prevent re-renders when returned object references change — wrap object selectors with useShallowdevelopment
Governs AI-assisted generation of images, video, and audio (Gemini Nano Banana Pro, FLUX, etc.) from any project, with a focus on key security and cost control. Trigger: generate an image/illustration/asset with AI, "generate an image", nano banana, gemini image, generate video, configure an image API.
business
Turns an already-investigated customer issue into a short, non-technical engineering-to-CS brief: one natural message that leads with the finding (root cause, real scope, open question), ready to paste into Slack for the support team. Trigger: /support-brief, support brief, brief for support, resumen soporte.
development
Parks the current Claude Code session before going to sleep. Analyzes the conversation, writes a handoff document to docs/handoffs/ in the current project, mirrors the same summary to engram tagged with the session ID, and prints the exact `claude --resume <id>` command for tomorrow. Zero interaction — runs end-to-end on a single invocation. Trigger: /night-park, night park, park session, me voy a dormir, guardar sesion.
testing
Interactive creator for a project feature: scaffolds the domain knowledge markdown at ai-context/features/<slug>.md AND the antenna skill at .claude/skills/<slug>/SKILL.md, both from the canonical templates. Also registers the antenna in the project's CLAUDE.md. Trigger: /feature-define <name>, define feature, documentar funcionalidad, nueva feature.