mcpField
S
state-management

state-management

github.com/mgd34msuDevelopment671 installsUpdated 11w ago
Repository

About state-management

A workflow skill for planning and implementing state management in TypeScript applications. It helps developers choose patterns for server, client, form, component, and URL state, then validate the result.

Key features

  1. 01State-library discovery
  2. 02State-type decision guide
  3. 03TanStack Query patterns
  4. 04Zustand store patterns
  5. 05React Hook Form and Zod forms
  6. 06URL-state implementation and validation

Use cases

  1. 01Choosing state tools for an existing codebase
  2. 02Adding cached API data and mutations
  3. 03Building validated complex forms
  4. 04Migrating state solutions and checking implementation

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/mgd34msu/goodvibes-plugin --skill state-management

After installing, type/state-managementin Claude Code to confirm it loaded.

Bundled filesreferences/state-patterns.mdscripts/validate-state.sh

Details

From the project README

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 TypeBest ToolUse When
Server stateTanStack QueryData from APIs, needs caching/invalidation
Client stateZustandUI state shared across components
Form stateReact Hook Form + ZodComplex forms with validation
URL statenuqs or searchParamsSharable, bookmarkable state
Component stateuseStateLocal 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.

Comments

Markdown supported