agentleFS
omspradippatilomspradippatil/Claude-Skills/.claude/skills/state-management/SKILL.md

state-management

Skill3 starsChanged 10 days agoMITView on GitHub

Activate this skill for complex frontend state, React Query/SWR data fetching, Zustand/Redux global stores, or XState.

We split this skill into the rules it gives an agent

2 rules. 0 are common, 2 are distinctive, and 0 are very specific to this project.

  • 0

    Common

    Most of the 21 edited versions we read keep these.

  • 2

    Distinctive

    This author's own choices. Worth a look.

  • 0

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Before picking a library, classify your state:

    line 11
  2. Distinctive:

    Use XState for multi-step forms, authentication flows, media players, drag-and-drop

    line 284

“Common” is measured: kept by at least 6 in 10 of the 21 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

State Management Skill

State Taxonomy

Before picking a library, classify your state:

TypeDefinitionBest Tool
Server stateData fetched from an API — async, cacheable, sharedTanStack Query, SWR
Global client stateApp-wide synchronous state (user session, theme, cart)Zustand, Redux Toolkit
URL stateState that lives in the URL (filters, pagination, tab)useSearchParams, router
Form stateEphemeral input values, validation errorsReact Hook Form
Local UI stateComponent-scoped toggles, open/close — not shareduseState, useReducer
Complex flow stateMulti-step wizards, async state machinesXState

Rule: reach for the simplest tool that fits. Local UI state belongs in useState, not a global store.


TanStack Query (React Query)

Query Keys

// Centralize query keys in a factory
export const postKeys = {
  all: ['posts'] as const,
  lists: () => [...postKeys.all, 'list'] as const,
  list: (filters: PostFilters) => [...postKeys.lists(), filters] as const,
  details: () => [...postKeys.all, 'detail'] as const,
  detail: (id: string) => [...postKeys.details(), id] as const,
};

// Usage
const { data } = useQuery({
  queryKey: postKeys.detail(postId),
  queryFn: () => api.getPost(postId),
  staleTime: 5 * 60 * 1000, // 5 minutes — don't refetch if fresh
});

Cache Invalidation

const queryClient = useQueryClient();

// After mutation: invalidate list queries (refetch in background)
const mutation = useMutation({
  mutationFn: api.createPost,
  onSuccess: () => {
    queryClient.invalidateQueries({ queryKey: postKeys.lists() });
  },
});

Optimistic Updates

const mutation = useMutation({
  mutationFn: api.updatePost,
  onMutate: async (updatedPost) => {
    // Cancel in-flight queries for this post
    await queryClient.cancelQueries({ queryKey: postKeys.detail(updatedPost.id) });

    // Snapshot previous value
    const previous = queryClient.getQueryData<Post>(postKeys.detail(updatedPost.id));

    // Optimistically update cache
    queryClient.setQueryData(postKeys.detail(updatedPost.id), updatedPost);

    return { previous }; // context for rollback
  },
  onError: (err, variables, context) => {
    // Rollback on error
    queryClient.setQueryData(postKeys.detail(variables.id), context?.previous);
  },
  onSettled: (_, __, variables) => {
    queryClient.invalidateQueries({ queryKey: postKeys.detail(variables.id) });
  },
});

Prefetching

// Prefetch on hover for instant navigation
const handleHover = () => {
  queryClient.prefetchQuery({
    queryKey: postKeys.detail(postId),
    queryFn: () => api.getPost(postId),
    staleTime: 10 * 1000,
  });
};

// Server-side prefetch (Next.js)
export async function getServerSideProps() {
  const queryClient = new QueryClient();
  await queryClient.prefetchQuery({ queryKey: postKeys.lists(), queryFn: api.getPosts });
  return { props: { dehydratedState: dehydrate(queryClient) } };
}

Stale-While-Revalidate Strategy

  • staleTime: how long data is considered fresh (no background refetch)
  • gcTime (cacheTime): how long unused data stays in cache before garbage collection
  • Pattern: staleTime: 60_000 (show cached data for 60s), gcTime: 5 * 60_000 (keep in memory for 5min)

SWR

import useSWR from 'swr';

const fetcher = (url: string) => fetch(url).then(r => r.json());

function PostDetail({ id }: { id: string }) {
  const { data, error, isLoading, mutate } = useSWR<Post>(`/api/posts/${id}`, fetcher, {
    revalidateOnFocus: true,       // refetch when window regains focus
    revalidateOnReconnect: true,   // refetch after going online
    dedupingInterval: 2000,        // dedupe requests within 2s
  });

  // Manual revalidation after mutation
  const handleDelete = async () => {
    await api.deletePost(id);
    mutate(undefined, { revalidate: false }); // clear cache
  };
}
  • SWR is simpler than TanStack Query; prefer TanStack Query for apps needing optimistic updates, dependent queries, or infinite scroll
  • SWR's mutate can be used globally: import { mutate } from 'swr'

Zustand

Slice Pattern

// features/auth/authSlice.ts
import { StateCreator } from 'zustand';

interface AuthSlice {
  user: User | null;
  signIn: (credentials: Credentials) => Promise<void>;
  signOut: () => void;
}

export const createAuthSlice: StateCreator<RootStore, [], [], AuthSlice> = (set) => ({
  user: null,
  signIn: async (credentials) => {
    const user = await authApi.signIn(credentials);
    set({ user });
  },
  signOut: () => set({ user: null }),
});

// store.ts — combine slices
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';

export const useStore = create<RootStore>()(
  devtools(
    persist(
      (...args) => ({
        ...createAuthSlice(...args),
        ...createCartSlice(...args),
      }),
      { name: 'app-store', partialize: (state) => ({ user: state.user }) } // only persist user
    ),
    { name: 'AppStore' }
  )
);

Derived State

// Derive outside the store using selectors to avoid over-subscription
const cartTotal = useStore((state) =>
  state.cartItems.reduce((sum, item) => sum + item.price * item.quantity, 0)
);

Devtools

  • Install Redux DevTools browser extension — Zustand devtools middleware connects automatically
  • Name your store actions for readable time-travel debugging: set({ count: count + 1 }, false, 'counter/increment')

Redux Toolkit

Slices

import { createSlice, PayloadAction } from '@reduxjs/toolkit';

const postsSlice = createSlice({
  name: 'posts',
  initialState: [] as Post[],
  reducers: {
    postAdded(state, action: PayloadAction<Post>) {
      state.push(action.payload); // Immer allows mutations
    },
    postRemoved(state, action: PayloadAction<string>) {
      return state.filter(p => p.id !== action.payload);
    },
  },
});

export const { postAdded, postRemoved } = postsSlice.actions;

RTK Query

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';

export const postsApi = createApi({
  reducerPath: 'postsApi',
  baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
  tagTypes: ['Post'],
  endpoints: (builder) => ({
    getPosts: builder.query<Post[], void>({
      query: () => '/posts',
      providesTags: ['Post'],
    }),
    createPost: builder.mutation<Post, Partial<Post>>({
      query: (body) => ({ url: '/posts', method: 'POST', body }),
      invalidatesTags: ['Post'], // auto-refetch getPosts after mutation
    }),
  }),
});

export const { useGetPostsQuery, useCreatePostMutation } = postsApi;

XState

State Machines for Complex Flows

import { createMachine, assign } from 'xstate';

const checkoutMachine = createMachine({
  id: 'checkout',
  initial: 'cart',
  context: { items: [], paymentError: null },
  states: {
    cart: {
      on: {
        PROCEED: 'address',
        ADD_ITEM: { actions: assign({ items: ({ context, event }) => [...context.items, event.item] }) },
      },
    },
    address: {
      on: { BACK: 'cart', SUBMIT: 'payment' },
    },
    payment: {
      on: {
        PAY: {
          target: 'processing',
          guard: ({ context }) => context.items.length > 0,
        },
        BACK: 'address',
      },
    },
    processing: {
      invoke: {
        src: 'processPayment',
        onDone: 'confirmation',
        onError: {
          target: 'payment',
          actions: assign({ paymentError: ({ event }) => event.error.message }),
        },
      },
    },
    confirmation: { type: 'final' },
  },
});
  • Use XState for multi-step forms, authentication flows, media players, drag-and-drop
  • Visualize machines with stately.ai — invaluable for complex flows
  • The actor model (spawn, spawnChild) handles parent-child machine communication

Form State — React Hook Form + Zod

import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';

const schema = z.object({
  email: z.string().email('Invalid email'),
  password: z.string().min(8, 'At least 8 characters'),
  age: z.coerce.number().int().min(18, 'Must be 18+'),
});

type FormValues = z.infer<typeof schema>;

function SignUpForm() {
  const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm<FormValues>({
    resolver: zodResolver(schema),
    defaultValues: { email: '', password: '' },
  });

  const onSubmit = async (data: FormValues) => {
    await api.signUp(data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register('email')} />
      {errors.email && <span>{errors.email.message}</span>}
      <button type="submit" disabled={isSubmitting}>Sign Up</button>
    </form>
  );
}

Anti-Patterns

Anti-PatternProblemFix
Prop drillingPassing props 4+ levels deepLift to Zustand/Context or co-locate
Over-fetchingFetching more data than the component needsSelect specific fields; use query params
Under-cachingFetching same data on every mountUse TanStack Query with appropriate staleTime
Everything in ReduxSimple local state in global storeKeep toggle state in useState
No query key structureAd-hoc string keys cause stale cache bugsUse query key factories
Missing loading/error statesUI hangs or crashes silentlyAlways handle isLoading and error in query hooks
Mutating Redux state directlyRuntime errors, missed updatesUse RTK slices (Immer) or ...spread
XState for simple togglesOver-engineeringuseState(false) is fine for a modal

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

Reviews can't be read right now.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.