Load PROACTIVELY when task involves application state, data fetching, or form handling. Use when user says "manage state", "add data fetching", "set up Zustand", "handle form validation", or "add React Query". Covers server state (TanStack Query with caching, optimistic updates), client state (Zustand stores), form state (React Hook Form with Zod validation), URL state (search params, routing), and choosing between state solutions.
Guides state architecture decisions and implements solutions for server, client, form, and URL state using appropriate libraries.
/plugin marketplace add mgd34msu/goodvibes-plugin/plugin install goodvibes@goodvibes-marketThis skill inherits all available tools. When active, it can use any tool Claude has access to.
references/state-patterns.mdscripts/validate-state.shscripts/
validate-state.sh
references/
state-patterns.md
This skill guides you through state architecture decisions and implementation using GoodVibes precision tools. Use this workflow when choosing state solutions, implementing data fetching patterns, or managing complex form state.
Load this skill when:
Trigger phrases: "state management", "TanStack Query", "Zustand", "React Hook Form", "form validation", "data fetching", "cache invalidation", "URL state".
Before choosing a state solution, understand existing patterns.
Use discover to find existing state management solutions.
discover:
queries:
- id: state_libraries
type: grep
pattern: "(from 'zustand'|from '@tanstack/react-query'|from 'react-hook-form'|from 'jotai'|from 'redux'|useContext)"
glob: "**/*.{ts,tsx}"
- id: data_fetching
type: grep
pattern: "(useQuery|useMutation|useSWR|fetch|axios)"
glob: "**/*.{ts,tsx}"
- id: form_libraries
type: grep
pattern: "(useForm|Formik|react-hook-form)"
glob: "**/*.{ts,tsx}"
verbosity: files_only
What this reveals:
Use precision_read to check what's installed.
precision_read:
files:
- path: "package.json"
extract: content
verbosity: minimal
Look for:
@tanstack/react-query (server state)zustand (client state)react-hook-form + zod (forms)nuqs or similar (URL state)Read 2-3 examples to understand implementation patterns.
precision_read:
files:
- path: "src/lib/query-client.ts" # or discovered file
extract: content
- path: "src/stores/user-store.ts" # or discovered file
extract: content
verbosity: standard
Choose the right tool for each type of state. See references/state-patterns.md for the complete decision tree.
| State Type | Best Tool | Use When |
|---|---|---|
| Server state | TanStack Query | Data from APIs, needs caching/invalidation |
| Client state | Zustand | UI state shared across components |
| Form state | React Hook Form + Zod | Complex forms with validation |
| URL state | nuqs or searchParams | Sharable, bookmarkable state |
| Component state | useState | Local to one component |
State Colocation Principle:
Keep state as close to where it's used as possible. Start with useState, lift to parent when shared, then consider dedicated solutions only when necessary.
For data from APIs that needs caching, background updates, and optimistic mutations.
Check if installed, otherwise add:
npm install @tanstack/react-query # Note: Targeting TanStack Query v5
npm install -D @tanstack/react-query-devtools
Create a query client configuration.
// src/lib/query-client.ts
import { QueryClient } from '@tanstack/react-query';
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60, // 1 minute
retry: 1,
refetchOnWindowFocus: false,
},
},
});
Basic Query:
import { useQuery } from '@tanstack/react-query';
import { getUser } from '@/lib/api';
export function useUser(userId: string) {
return useQuery({
queryKey: ['user', userId],
queryFn: () => getUser(userId),
enabled: !!userId, // Don't run if no userId
});
}
Mutation with Optimistic Update:
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { updateUser } from '@/lib/api';
export function useUpdateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: updateUser,
onMutate: async (newUser) => {
// Cancel outgoing queries
await queryClient.cancelQueries({ queryKey: ['user', newUser.id] });
// Snapshot previous value
const previousUser = queryClient.getQueryData(['user', newUser.id]);
// Optimistically update
queryClient.setQueryData(['user', newUser.id], newUser);
return { previousUser };
},
onError: (err, newUser, context) => {
// Rollback on error
queryClient.setQueryData(
['user', newUser.id],
context?.previousUser
);
},
onSettled: (data, error, variables) => {
// Refetch after error or success
queryClient.invalidateQueries({ queryKey: ['user', variables.id] });
},
});
}
Cache Invalidation:
// Invalidate all user queries
queryClient.invalidateQueries({ queryKey: ['user'] });
// Invalidate specific user
queryClient.invalidateQueries({ queryKey: ['user', userId] });
// Remove from cache entirely
queryClient.removeQueries({ queryKey: ['user', userId] });
Consuming Error and Loading States:
function UserProfile({ userId }: { userId: string }) {
const { data, isPending, isError, error } = useUser(userId);
if (isPending) return <Skeleton />;
if (isError) return <ErrorDisplay error={error} />;
return <UserProfile user={data} />;
}
For UI state shared across components (modals, themes, filters).
npm install zustand
Simple Store:
import { create } from 'zustand';
interface UIStore {
sidebarOpen: boolean;
toggleSidebar: () => void;
theme: 'light' | 'dark';
setTheme: (theme: 'light' | 'dark') => void;
}
export const useUIStore = create<UIStore>((set) => ({
sidebarOpen: true,
toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })),
theme: 'light',
setTheme: (theme) => set({ theme }),
}));
Store with Slices:
import { create, StateCreator } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
// Slice pattern for organization
interface AuthSlice {
user: User | null;
setUser: (user: User | null) => void;
}
interface UISlice {
sidebarOpen: boolean;
toggleSidebar: () => void;
}
type Store = AuthSlice & UISlice;
const createAuthSlice: StateCreator<Store, [], [], AuthSlice> = (set) => ({
user: null,
setUser: (user) => set({ user }),
});
const createUISlice: StateCreator<Store, [], [], UISlice> = (set) => ({
sidebarOpen: true,
toggleSidebar: () => set((state: Store) => ({
sidebarOpen: !state.sidebarOpen
})),
});
export const useStore = create<Store>()(
devtools(
persist(
(...a) => ({
...createAuthSlice(...a),
...createUISlice(...a),
}),
{ name: 'app-store' }
)
)
);
Using Selectors:
// Avoid re-renders by selecting only what you need
const sidebarOpen = useUIStore((state) => state.sidebarOpen);
const toggleSidebar = useUIStore((state) => state.toggleSidebar);
For complex forms with validation, field arrays, and nested objects.
npm install react-hook-form zod @hookform/resolvers
import { z } from 'zod';
export const userSchema = z.object({
email: z.string().email('Invalid email address'),
name: z.string().min(2, 'Name must be at least 2 characters'),
age: z.number().min(18, 'Must be 18 or older'),
role: z.enum(['user', 'admin']).default('user'),
preferences: z.object({
newsletter: z.boolean().default(false),
notifications: z.boolean().default(true),
}),
});
export type UserFormData = z.infer<typeof userSchema>;
Basic Form:
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { userSchema, type UserFormData } from './schema';
export function UserForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<UserFormData>({
resolver: zodResolver(userSchema),
defaultValues: {
role: 'user',
preferences: {
newsletter: false,
notifications: true,
},
},
});
const onSubmit = async (data: UserFormData) => {
await createUser(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('email')} />
{errors.email && <span>{errors.email.message}</span>}
<input {...register('name')} />
{errors.name && <span>{errors.name.message}</span>}
<input type="number" {...register('age', { valueAsNumber: true })} />
{errors.age && <span>{errors.age.message}</span>}
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Submitting...' : 'Submit'}
</button>
</form>
);
}
Field Arrays:
import { useFieldArray } from 'react-hook-form';
const schema = z.object({
users: z.array(
z.object({
name: z.string().min(1),
email: z.string().email(),
})
).min(1, 'At least one user required'),
});
function UsersForm() {
const { control, register } = useForm({
resolver: zodResolver(schema),
});
const { fields, append, remove } = useFieldArray({
control,
name: 'users',
});
return (
<div>
{fields.map((field, index) => (
<div key={field.id}>
<input {...register(`users.${index}.name`)} />
<input {...register(`users.${index}.email`)} />
<button type="button" onClick={() => remove(index)}>
Remove
</button>
</div>
))}
<button
type="button"
onClick={() => append({ name: '', email: '' })}
>
Add User
</button>
</div>
);
}
For state that should be shareable and bookmarkable.
npm install nuqs # Note: Targeting nuqs v1.x
import { useQueryState, parseAsInteger, parseAsStringEnum } from 'nuqs';
export function ProductList() {
const [page, setPage] = useQueryState(
'page',
parseAsInteger.withDefault(1)
);
const [sort, setSort] = useQueryState(
'sort',
parseAsStringEnum(['name', 'price', 'date']).withDefault('name')
);
// URL: /products?page=2&sort=price
// Automatically synced, type-safe, bookmarkable
}
import { useSearchParams, useRouter } from 'next/navigation';
export function ProductList() {
const searchParams = useSearchParams();
const router = useRouter();
const page = Number(searchParams.get('page')) || 1;
const setPage = (newPage: number) => {
const params = new URLSearchParams(searchParams);
params.set('page', String(newPage));
router.push(`?${params.toString()}`);
};
}
Use precision_write to create state management files.
precision_write:
files:
- path: "src/lib/query-client.ts"
content: |
import { QueryClient } from '@tanstack/react-query';
export const queryClient = new QueryClient({ ... });
- path: "src/stores/ui-store.ts"
content: |
import { create } from 'zustand';
export const useUIStore = create({ ... });
- path: "src/schemas/user-schema.ts"
content: |
import { z } from 'zod';
export const userSchema = z.object({ ... });
verbosity: count_only
Use the validation script to ensure quality.
bash scripts/validate-state.sh .
See scripts/validate-state.sh for the complete validation suite.
Verify TypeScript compilation.
precision_exec:
commands:
- cmd: "npm run typecheck"
expect:
exit_code: 0
verbosity: minimal
// Server data with TanStack Query
const { data: user } = useQuery({ queryKey: ['user'], queryFn: getUser });
// UI state with Zustand
const { sidebarOpen, toggleSidebar } = useUIStore();
const mutation = useMutation({
mutationFn: createUser,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
},
});
const onSubmit = (data: UserFormData) => {
mutation.mutate(data);
};
const [page] = useQueryState('page', parseAsInteger.withDefault(1));
const [search] = useQueryState('search');
const { data } = useQuery({
queryKey: ['products', page, search],
queryFn: () => getProducts({ page, search }),
});
DON'T:
any types in state storesDO:
Discovery Phase:
discover: { queries: [state_libraries, data_fetching, forms], verbosity: files_only }
precision_read: { files: ["package.json", example stores], extract: content }
Implementation Phase:
precision_write: { files: [query-client, stores, schemas], verbosity: count_only }
Validation Phase:
precision_exec: { commands: [{ cmd: "npm run typecheck" }], verbosity: minimal }
Post-Implementation:
bash scripts/validate-state.sh .
For detailed patterns and decision trees, see references/state-patterns.md.
Activates when the user asks about AI prompts, needs prompt templates, wants to search for prompts, or mentions prompts.chat. Use for discovering, retrieving, and improving prompts.
Search, retrieve, and install Agent Skills from the prompts.chat registry using MCP tools. Use when the user asks to find skills, browse skill catalogs, install a skill for Claude, or extend Claude's capabilities with reusable AI agent components.
Creating algorithmic art using p5.js with seeded randomness and interactive parameter exploration. Use this when users request creating art using code, generative art, algorithmic art, flow fields, or particle systems. Create original algorithmic art rather than copying existing artists' work to avoid copyright violations.