Resources
scripts/ validate-state.sh references/ state-patterns.md
State Management
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.
When to Use This Skill
Load this skill when:
-
Deciding between state management approaches
-
Implementing server data fetching with caching
-
Building forms with complex validation
-
Managing client-side application state
-
Implementing URL-based state patterns
-
Migrating from one state solution to another
Trigger phrases: "state management", "TanStack Query", "Zustand", "React Hook Form", "form validation", "data fetching", "cache invalidation", "URL state".
Core Workflow
Phase 1: Discovery
Before choosing a state solution, understand existing patterns.
Step 1.1: Identify Current State Libraries
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:
-
Existing state management libraries in use
-
Data fetching patterns (REST, GraphQL, etc.)
-
Form handling approaches
-
Consistency across the codebase
Step 1.2: Analyze Package Dependencies
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)
Step 1.3: Read Existing State Patterns
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
Phase 2: State Categorization
Choose the right tool for each type of state. See references/state-patterns.md for the complete decision tree.
Quick Decision Guide
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.
Phase 3: Server State with TanStack Query
For data from APIs that needs caching, background updates, and optimistic mutations.
Step 3.1: Install Dependencies
Check if installed, otherwise add:
npm install @tanstack/react-query # Note: Targeting TanStack Query v5 npm install -D @tanstack/react-query-devtools
Step 3.2: Set Up Query Client
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, }, }, });
Step 3.3: Implement Query Patterns
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} />; }
Phase 4: Client State with Zustand
For UI state shared across components (modals, themes, filters).
Step 4.1: Install Dependencies
npm install zustand
Step 4.2: Create Store
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);
Phase 5: Form State with React Hook Form + Zod
For complex forms with validation, field arrays, and nested objects.
Step 5.1: Install Dependencies
npm install react-hook-form zod @hookform/resolvers
Step 5.2: Define Validation Schema
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>;
Step 5.3: Implement Form
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>
);
}
Phase 6: URL State Patterns
For state that should be shareable and bookmarkable.
Step 6.1: Using nuqs (Recommended)
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 }
Step 6.2: Using Next.js searchParams
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()});
};
}
Phase 7: Implementation with Precision Tools
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
Phase 8: Validation
Step 8.1: Run Validation Script
Use the validation script to ensure quality.
bash scripts/validate-state.sh .
See scripts/validate-state.sh for the complete validation suite.
Step 8.2: Type Check
Verify TypeScript compilation.
precision_exec: commands: - cmd: "npm run typecheck" expect: exit_code: 0 verbosity: minimal
Common Patterns
Pattern 1: Combine TanStack Query + Zustand
// Server data with TanStack Query const { data: user } = useQuery({ queryKey: ['user'], queryFn: getUser });
// UI state with Zustand const { sidebarOpen, toggleSidebar } = useUIStore();
Pattern 2: Form with Server Mutation
const mutation = useMutation({ mutationFn: createUser, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['users'] }); }, });
const onSubmit = (data: UserFormData) => { mutation.mutate(data); };
Pattern 3: URL State Driving Data Fetching
const [page] = useQueryState('page', parseAsInteger.withDefault(1)); const [search] = useQueryState('search');
const { data } = useQuery({ queryKey: ['products', page, search], queryFn: () => getProducts({ page, search }), });
Common Anti-Patterns
DON'T:
-
Use global state for server data (use TanStack Query)
-
Put everything in Zustand (colocate when possible)
-
Manage form state manually (use React Hook Form)
-
Store UI state in URL params (use Zustand)
-
Use Context for frequently changing data (causes re-renders)
-
Forget to invalidate cache after mutations
-
Skip validation schemas for forms
-
Use any types in state stores
DO:
-
Match state type to the right tool
-
Colocate state when possible
-
Use selectors to prevent re-renders
-
Implement optimistic updates for better UX
-
Validate all form inputs with Zod
-
Use TypeScript for all state definitions
-
Invalidate queries after mutations
-
Keep query keys consistent
Quick Reference
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 .