AgentSkillsCN

state-management

React应用的状态管理模式

SKILL.md
--- frontmatter
name: state-management
description: State management patterns for React applications
version: 1.0.0
type: domain
enforcement: suggest
priority: high
triggers:
  - useState
  - useQuery
  - zustand
  - context
  - global state
  - server state
  - cache
  - TanStack Query

State Management Guidelines

Overview

This skill provides patterns for managing state in React applications, distinguishing between server state (API data) and client state (UI state).

Quick Reference

State TypeToolUse Case
Server StateTanStack QueryAPI data, caching
Global ClientZustandApp settings, UI state
Local ClientuseStateComponent-specific
Shared PropsReact ContextTheme, auth, i18n

The Golden Rule

Server state is NOT client state.

typescript
// ❌ WRONG: Treating API data as client state
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);

useEffect(() => {
  setLoading(true);
  fetchUsers()
    .then(setUsers)
    .catch(setError)
    .finally(() => setLoading(false));
}, []);

// ✅ CORRECT: Server state with TanStack Query
const { data: users, isLoading, error } = useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,
});

Project Configuration

<!-- CUSTOMIZE START -->
SettingDefaultYour Value
TanStack Query Versionv5CHANGE_ME
Zustand Versionv4CHANGE_ME
Stale Time5 minutesCHANGE_ME
GC Time30 minutesCHANGE_ME
<!-- CUSTOMIZE END -->

State Decision Tree

code
Need to manage state?
├── Is it from an API/server?
│   └── YES → TanStack Query
│       └── Handles: caching, loading, error, refetch
│
├── Is it used by multiple components across the app?
│   └── YES → Is it frequently updated?
│       ├── YES → Zustand (subscriptions, selectors)
│       └── NO → React Context (theme, locale, auth)
│
└── Is it local to one component/feature?
    └── YES → useState / useReducer

Core Patterns

Pattern 1: TanStack Query Setup

typescript
// providers/QueryProvider.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000, // 5 minutes
      gcTime: 30 * 60 * 1000, // 30 minutes (formerly cacheTime)
      retry: 3,
      refetchOnWindowFocus: false,
    },
    mutations: {
      retry: 1,
    },
  },
});

export function QueryProvider({ children }: { children: React.ReactNode }) {
  return (
    <QueryClientProvider client={queryClient}>
      {children}
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  );
}

Pattern 2: Query Hooks

typescript
// ✅ CORRECT: Encapsulated query hook
// hooks/queries/useUsers.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { userApi } from '@/services/api';

export const userKeys = {
  all: ['users'] as const,
  lists: () => [...userKeys.all, 'list'] as const,
  list: (filters: UserFilters) => [...userKeys.lists(), filters] as const,
  details: () => [...userKeys.all, 'detail'] as const,
  detail: (id: string) => [...userKeys.details(), id] as const,
};

export function useUsers(filters: UserFilters) {
  return useQuery({
    queryKey: userKeys.list(filters),
    queryFn: () => userApi.getUsers(filters),
  });
}

export function useUser(id: string) {
  return useQuery({
    queryKey: userKeys.detail(id),
    queryFn: () => userApi.getUser(id),
    enabled: !!id,
  });
}

export function useCreateUser() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: userApi.createUser,
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: userKeys.lists() });
    },
  });
}

Pattern 3: Zustand Store

typescript
// ✅ CORRECT: Zustand store with slices
// stores/appStore.ts
import { create } from 'zustand';
import { persist } from 'zustand/middleware';

interface AppState {
  // State
  theme: 'light' | 'dark';
  sidebarOpen: boolean;
  notifications: Notification[];

  // Actions
  setTheme: (theme: 'light' | 'dark') => void;
  toggleSidebar: () => void;
  addNotification: (notification: Notification) => void;
  removeNotification: (id: string) => void;
}

export const useAppStore = create<AppState>()(
  persist(
    (set) => ({
      // Initial state
      theme: 'light',
      sidebarOpen: true,
      notifications: [],

      // Actions
      setTheme: (theme) => set({ theme }),
      toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })),
      addNotification: (notification) =>
        set((state) => ({
          notifications: [...state.notifications, notification],
        })),
      removeNotification: (id) =>
        set((state) => ({
          notifications: state.notifications.filter((n) => n.id !== id),
        })),
    }),
    {
      name: 'app-storage',
      partialize: (state) => ({ theme: state.theme }), // Only persist theme
    }
  )
);

Pattern 4: Selective Subscriptions

typescript
// ✅ CORRECT: Only subscribe to needed state
function ThemeToggle() {
  // Component only re-renders when theme changes
  const theme = useAppStore((state) => state.theme);
  const setTheme = useAppStore((state) => state.setTheme);

  return (
    <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
      {theme === 'light' ? '🌙' : '☀️'}
    </button>
  );
}

// ❌ WRONG: Subscribing to entire store
function ThemeToggle() {
  // Re-renders on ANY store change
  const { theme, setTheme } = useAppStore();
  // ...
}

Pattern 5: Context for Dependency Injection

typescript
// ✅ CORRECT: Context for stable values
// contexts/AuthContext.tsx
interface AuthContextValue {
  user: User | null;
  isAuthenticated: boolean;
  login: (credentials: LoginCredentials) => Promise<void>;
  logout: () => void;
}

const AuthContext = createContext<AuthContextValue | null>(null);

export function AuthProvider({ children }: { children: React.ReactNode }) {
  const [user, setUser] = useState<User | null>(null);

  const value = useMemo(
    () => ({
      user,
      isAuthenticated: !!user,
      login: async (credentials: LoginCredentials) => {
        const user = await authService.login(credentials);
        setUser(user);
      },
      logout: () => {
        authService.logout();
        setUser(null);
      },
    }),
    [user]
  );

  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}

export function useAuth() {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth must be used within AuthProvider');
  }
  return context;
}

Anti-patterns Summary

Anti-patternProblemSolution
useState for API dataManual cache, no deduplicationTanStack Query
Full store subscriptionUnnecessary re-rendersSelective selectors
Context for frequent updatesProvider re-renders treeZustand
Props drillingBrittle, hard to maintainContext or Zustand
Redux for everythingBoilerplate overheadRight tool for job

Common Mistakes

typescript
// ❌ WRONG: Derived state in store
const useStore = create((set) => ({
  items: [],
  filteredItems: [], // Derived!
  filter: '',
  setFilter: (filter) => set((state) => ({
    filter,
    filteredItems: state.items.filter(i => i.name.includes(filter)),
  })),
}));

// ✅ CORRECT: Compute derived state
const useStore = create((set) => ({
  items: [],
  filter: '',
  setFilter: (filter) => set({ filter }),
}));

// Use selector for derived state
const filteredItems = useStore((state) =>
  state.items.filter((i) => i.name.includes(state.filter))
);

Resources

TopicWhen to ReadLink
TanStack QueryServer state patterns[mdc:resources/tanstack-query.md]
Zustand PatternsClient state patterns[mdc:resources/zustand-patterns.md]
Context PatternsWhen to use Context[mdc:resources/context-patterns.md]

Related Skills

  • frontend-dev-guidelines - Component patterns
  • api-validation - Type-safe API calls
  • testing-guidelines - Testing state hooks