Die Oberfläche ist lokalisiert; Skill-Beschreibungen und -Dokumente werden per KI automatisch in Ihre Sprache übersetzt. Code, Befehle, Pfade und Fachbegriffe bleiben im Original, maßgeblich ist das Dokument des Autors.
Skills entdecken
Design und Kreativität · SKILL.md

frontend-state-management

frontend-state-management

Use this skill when the user says 'state management', 'global state', 'Redux', 'Zustand', 'Pinia', 'NgRx', 'React Context', 'state architecture', 'where to put state', 'server state vs client state', or when deciding how to manage application state. This skill enforces: server state goes in server state libraries (TanStack Query, SWR, Apollo), local state g…

Independent GitHub snapshot · Automated classification · Original documentation governs

Skill-Dokumentation

GitHub

name: frontend-state-management description: > Use this skill when the user says 'state management', 'global state', 'Redux', 'Zustand', 'Pinia', 'NgRx', 'React Context', 'state architecture', 'where to put state', 'server state vs client state', or when deciding how to manage application state. This skill enforces: server state goes in server state libraries (TanStack Query, SWR, Apollo), local state goes in useState/ref/signals, shared state goes in lightweight stores (Zustand, Pinia, Signal Store), and complex state goes in XState or Redux Toolkit. Works with React, Vue, Angular. Do NOT use for: database design, backend caching, or API design. version: "2.0.0" author: "j4flmao" license: "MIT" compatibility: claude-code: true cursor: true codex: true windsurf: true tags: [frontend, state, phase-3, universal]

Frontend State Management

Purpose

Choose and implement the correct state management pattern for each type of state. Server state, client state, local state, and URL state each have their own solution. Never put server state in a global store.

Agent Protocol

Trigger

Exact user phrases: "state management", "global state", "Redux", "Zustand", "Pinia", "NgRx", "React Context", "state architecture", "where to put state", "server state vs client state", "state library".

Input Context

Before activating, verify:

  • The framework is known (React, Vue, Angular).
  • The type of state being discussed is identified (server data, UI state, form state, URL state).

Output Artifact

No file output. Produces state architecture decision as text.

Response Format

State: {description}
Type: {server/client/URL}
Storage: {local/global/shared}
Library: {specific library}
Location: {file path}
Rationale: {one sentence}

No preamble. No postamble. No explanations. No filler/hedging/transitions. Compress output — why use many token when few do trick.

Completion Criteria

  • State type correctly classified (server vs client).
  • Decision follows the decision tree below.
  • Server state uses a server state library, NOT a global store.
  • Global stores are split by domain (one store per feature, not one store for everything).
  • Context is used for dependency injection, not state management.
  • URL state is not duplicated in component state.

Max Response Length

Per state decision: 7 lines.

State Management Architecture / Decision Trees

State Classification Decision Tree

Where is the source of truth?
  |-- On the server (database, API, third-party service) -->
  |     SERVER STATE
  |     Library: TanStack Query, SWR, Apollo, RTK Query
  |     NEVER put in Zustand, Redux, Pinia, or Context
  |
  |-- On the client (user interaction, UI state) -->
  |     Is it shared across multiple components?
  |     |-- NO -->
  |     |     LOCAL STATE
  |     |     React: useState, useReducer
  |     |     Vue: ref, reactive
  |     |     Angular: Component-local signal
  |     |
  |     |-- YES -->
  |           |-- Complex (multiple actions, transitions, side effects)? -->
  |           |     |-- YES -->
  |           |     |     COMPLEX STATE (state machine)
  |           |     |     Library: XState, Redux Toolkit (slices + thunks), NgRx
  |           |     |     Use when: 15+ actions, cross-cutting concerns, middleware needed
  |           |     |
  |           |     |-- NO -->
  |           |           SHARED STATE (simple store)
  |           |           React: Zustand, Jotai, Valtio
  |           |           Vue: Pinia
  |           |           Angular: Signal Store
  |           |
  |           |-- Derived / computed from other state?
  |                 COMPUTED STATE
  |                 React: useMemo, Zustand selectors, Jotai derived atoms
  |                 Vue: computed()
  |                 Angular: computed signal
  |
  |-- In the URL (search params, path, hash) -->
  |     URL STATE
  |     Read: useSearchParams, useParams, useLocation
  |     Write: router.push/replace, searchParams set
  |     NEVER duplicate URL state in component state
  |
  |-- On the device (localStorage, IndexedDB, cookies) -->
        PERSISTED STATE
        Sync to store on init, write back on change
        Zod schema validation on read (storage can be tampered)

Global Store Structure Decision Tree

How many pieces of state?
  |-- 1-3 related values (auth: user, token, isAuthenticated) -->
  |     Single small store is fine
  |
  |-- 4-10 related values -->
  |     Split by domain if they change independently
  |     |-- auth: user, token, login, logout
  |     |-- cart: items, total, addItem, removeItem
  |     |-- ui: sidebarOpen, theme, modalState
  |
  |-- 10+ values -->
        MUST split by domain
        Each store: max 5-7 state properties + associated actions
        A store with 20+ properties is a design smell

Workflow

Step 1: Classify State

Is the source of truth on the server?
  YES -> Server state -> useQuery / useFetch / Apollo
  NO  -> Is it shared across multiple components?
    NO  -> Component local -> useState / ref / signal
    YES -> Is it complex (multiple actions, transitions)?
      NO  -> Simple store -> Zustand / Pinia / Signal Store
      YES -> State machine -> XState / Redux Toolkit / NgRx

Step 2: Server State Pattern

function useUsers() {
  return useQuery({
    queryKey: ['users', { page }],
    queryFn: () => api.getUsers({ page }),
    staleTime: 30_000,
    gcTime: 5 * 60_000,
  })
}

Server state rules:

  • staleTime prevents unnecessary refetches. Set based on data volatility.
  • gcTime keeps data in cache for instant back-navigation.
  • Mutations invalidate related queries on success. Never manually set query data.

Step 3: Client State Patterns

// Local state (one component)
function Counter() {
  const [count, setCount] = useState(0)
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>
}

// Shared state (multiple components)
const useAuthStore = create<AuthState>((set) => ({
  user: null,
  login: async (credentials) => {
    const user = await api.login(credentials)
    set({ user })
  },
  logout: () => set({ user: null }),
}))

// Complex state machine
const orderMachine = createMachine({
  id: 'order',
  initial: 'idle',
  states: {
    idle:      { on: { SUBMIT: 'loading' } },
    loading:   { on: { SUCCESS: 'success', ERROR: 'error' } },
    success:   { on: { RESET: 'idle' } },
    error:     { on: { RETRY: 'loading', RESET: 'idle' } },
  },
})

Step 4: State Location Rules

State TypeStorageExample
Form inputComponent localuseState in form component
Theme preferenceGlobal store (small)useThemeStore
Auth statusGlobal store (small)useAuthStore
API response dataServer state libraryuseQuery
Modal open/closeComponent localuseState
Real-time dataServer state subscriptionuseQuery with WebSocket
URL paramsURL/search paramsuseSearchParams
Form validationComponent local or form libraryreact-hook-form

Step 5: Zustand Store Example

import { create } from 'zustand'
import { devtools, persist } from 'zustand/middleware'

interface CartStore {
  items: CartItem[]
  totalItems: number
  addItem: (item: CartItem) => void
  removeItem: (id: string) => void
  clearCart: () => void
}

const useCartStore = create<CartStore>()(
  devtools(
    persist(
      (set, get) => ({
        items: [],
        totalItems: 0,
        addItem: (item) => set((state) => ({
          items: [...state.items, item],
          totalItems: state.totalItems + 1,
        })),
        removeItem: (id) => set((state) => ({
          items: state.items.filter((i) => i.id !== id),
          totalItems: state.totalItems - 1,
        })),
        clearCart: () => set({ items: [], totalItems: 0 }),
      }),
      { name: 'cart-storage' }
    )
  )
)

Step 6: State Testing Patterns

// Test Zustand store
describe('CartStore', () => {
  beforeEach(() => {
    useCartStore.setState({ items: [], totalItems: 0 })
  })

  it('adds item to cart', () => {
    const item = { id: '1', name: 'Test', price: 10 }
    useCartStore.getState().addItem(item)
    expect(useCartStore.getState().items).toHaveLength(1)
  })

  it('removes item from cart', () => {
    const item = { id: '1', name: 'Test', price: 10 }
    useCartStore.getState().addItem(item)
    useCartStore.getState().removeItem('1')
    expect(useCartStore.getState().items).toHaveLength(0)
  })
})

Common Pitfalls

1. Server State in Global Store

// BAD -- putting API data in Zustand
const useUserStore = create((set) => ({
  user: null,
  fetchUser: async (id) => {
    const user = await api.getUser(id)
    set({ user }) // Manual cache, no dedup, no refetch
  },
}))

// GOOD -- use server state library
function useUser(id: string) {
  return useQuery({
    queryKey: ['user', id],
    queryFn: () => api.getUser(id),
    staleTime: 30_000,
  })
}

2. Context for State Management

Context triggers re-render of all consumers on any value change. For frequently-updated state, use Zustand or Jotai which use subscriptions instead of prop drilling.

3. Monolithic Global Store

A single Redux store with 50+ slices creates unnecessary coupling. Zustand or Pinia encourages multiple small stores by design.

4. Duplicating URL State

Reading search params into useState creates sync issues. Always derive from URL, write to URL.

5. Not Normalizing Server State

Cached server state should be normalized by ID to avoid duplication. TanStack Query handles this with queryKey.

Compared With

LibraryBundle SizeBoilerplateFrameworkBest For
useState0KBLowReactLocal component state
useReducer0KBMediumReactLocal complex state
Zustand~1KBLowAnyShared simple state
Jotai~3KBLowReactAtomic shared state
Redux Toolkit~12KBMediumReactComplex app state
Pinia~1KBLowVueVue shared state
NgRx~15KBHighAngularComplex Angular state
TanStack Query~13KBLowAnyServer state
XState~12KBMediumAnyState machines

Performance Considerations

Store Selection Performance Impact

ApproachRe-renders on changeSetup costMemoization needed
ContextAll consumersLowYes (value memo)
ZustandOnly subscribed componentsLowNo (selector-based)
JotaiOnly dependent atomsLowNo (fine-grained)
ReduxConnected components via selectorMediumYes (reselect)
PiniaOnly subscribed componentsLowNo
NgRxConnected componentsHighYes (selectors)

Rule of Thumb

  • If state is read > written (theme, locale, auth): Context (with memoized value) is fine
  • If state is written > read (form inputs, UI toggles): Zustand, useState, or ref
  • If state changes rapidly (animations, real-time): Jotai atoms or signals

Accessibility Considerations

  • State changes should trigger appropriate aria-live announcements (loading, error, content updates)
  • Modal/dialog state management must manage focus (trap focus when open, restore on close)
  • Theme state changes should not cause sudden visual changes without user awareness

Security Considerations

  • Persisted stores (localStorage) should validate data with Zod on read — storage can be tampered
  • Auth state should never be persisted to localStorage. Use httpOnly cookies + server state.
  • User data in stores should be sanitized before rendering (avoid XSS via stored state)

Rules

  • Server state goes in TanStack Query / SWR / Apollo. Never put server state in Zustand, Redux, or Pinia.
  • Split global stores by domain. One store per feature. A store with 20+ properties should be split.
  • React Context is dependency injection, not a state management solution. Use it for theme providers, locale providers, not for app state.
  • Never store derived data in state. Compute it with selectors, computed, or useMemo.
  • URL parameters are state. Do not duplicate URL state in component state.
  • If two pieces of state change together, they belong together. If they change independently, split them.

References

  • references/client-state.md — Client State
  • references/server-state.md — Server State
  • references/state-architecture.md — State Architecture Patterns
  • references/state-comparison.md — State Management Library Comparison
  • references/state-patterns.md — State Management Patterns
  • references/state-testing.md — State Testing Patterns

Handoff

No artifact produced. Next skill: frontend-performance — optimize rendering and bundle size. Carry forward: state architecture decisions, store structure, server state configuration.

Implementation Patterns

State Store Factory

// Generic store factory for Zustand
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';

interface StoreConfig<T> {
  name: string;
  initialState: T;
  persist?: boolean;
  devtools?: boolean;
}

export function createStore<T extends Record<string, unknown>>(
  config: StoreConfig<T>,
  actions: (set: any, get: any) => Record<string, (...args: any[]) => void>
) {
  const middlewares: any[] = [];

  if (config.devtools) {
    middlewares.push(devtools);
  }

  if (config.persist) {
    middlewares.push(persist);
  }

  middlewares.push(immer);

  const base = (set: any, get: any) => ({
    ...config.initialState,
    ...actions(set, get),
  });

  const composed = middlewares.reduce((fn, middleware) => middleware(fn), base);
  return create(composed, { name: config.name });
}

// Usage example
interface UIState {
  sidebarOpen: boolean;
  theme: 'light' | 'dark';
  modalId: string | null;
}

const useUIStore = createStore<UIState>(
  {
    name: 'ui-store',
    initialState: {
      sidebarOpen: true,
      theme: 'light',
      modalId: null,
    },
    persist: true,
    devtools: true,
  },
  (set) => ({
    toggleSidebar: () =>
      set((state: UIState) => {
        state.sidebarOpen = !state.sidebarOpen;
      }),
    setTheme: (theme: 'light' | 'dark') =>
      set((state: UIState) => {
        state.theme = theme;
      }),
    openModal: (id: string) =>
      set((state: UIState) => {
        state.modalId = id;
      }),
    closeModal: () =>
      set((state: UIState) => {
        state.modalId = null;
      }),
  })
);

Selector Utilities

// createSelector with memoization
import { createSelector } from 'reselect';

interface Todo {
  id: string;
  text: string;
  completed: boolean;
  userId: string;
}

interface RootState {
  todos: Todo[];
  filter: 'all' | 'active' | 'completed';
}

const selectTodos = (state: RootState) => state.todos;
const selectFilter = (state: RootState) => state.filter;

export const selectFilteredTodos = createSelector(
  [selectTodos, selectFilter],
  (todos, filter) => {
    switch (filter) {
      case 'active':
        return todos.filter(t => !t.completed);
      case 'completed':
        return todos.filter(t => t.completed);
      default:
        return todos;
    }
  }
);

export const selectTodoStats = createSelector(
  [selectTodos],
  (todos) => ({
    total: todos.length,
    completed: todos.filter(t => t.completed).length,
    active: todos.filter(t => !t.completed).length,
    completionRate: todos.length > 0
      ? Math.round((todos.filter(t => t.completed).length / todos.length) * 100)
      : 0,
  })
);

// Partial state subscription — only re-render on relevant changes
export const useTodoById = (id: string) => {
  return useTodoStore((state) => state.todos.find(t => t.id === id));
};

Architecture Decision Trees

State Management Selection

What type of state?
├── Server state (API data, database)
│   └── TanStack Query / SWR / RTK Query
│       ├── Automatic caching, refetching, pagination
│       ├── Optimistic updates for mutations
│       └── Cache invalidation via query keys
│
├── Client state (UI, form inputs)
│   ├── Simple component state → useState / useReducer
│   ├── Shared UI state (sidebar, modals) → Zustand / Jotai
│   └── Complex form state → React Hook Form / Formik
│
├── URL state (filters, search, pagination)
│   └── URLSearchParams + next/navigation or react-router
│       ├── Bookmarkable, shareable URLs
│       └── Back/forward navigation works
│
└── Persistent state (preferences, theme)
    └── Zustand persist middleware / localStorage
        ├── Automatic serialization
        └── Validation on read (handle tampered storage)

Anti-Patterns

Anti-PatternWhy It FailsCorrect Approach
Putting everything in global storeComponent-specific state pollutes global scopeComponent state for UI, global for shared
Mutating server state in client storeStale data, inconsistency with backendServer state tools handle synchronization
No selected patternDifferent patterns in every filePick one approach per state type
React Context for everythingPerformance issues from re-rendersContext for DI (theme, locale), not state
Storing derived dataInconsistency when source changesCompute with selectors/memoization
Over-normalized stateComplex selectors for simple readsKeep denormalized where practical
Ignoring stale-while-revalidateUI shows stale data indefinitelySWR pattern: show cached, refresh in background

Performance Optimization

  • Zustand shallow comparison: Use useStore(state => ({...}), shallow) to avoid unnecessary re-renders when selecting multiple values. Shallow compare checks reference equality for each property.
  • Jotai atom splitting: Split large atoms into small focused atoms. Only re-render subscribers of the changed atom. 10 small atoms outperform 1 large store for independent values.
  • TanStack Query stale time: Set staleTime to avoid refetching data that doesn't change often. Use cacheTime to control GC of unused query results.
  • Memoized selectors: Use createSelector (Reselect) for derived data. Avoids recomputation unless input selectors change. Essential for expensive computations.
Gespeicherte Skills