cursor / claude rules

React Zustand Cursor Rules

Cursor rules for React + Zustand client state. Store ownership boundaries, selectors, middleware, persistence, and testing.
by cache.directory updated Sep 10, 2026 CC-BY-4.0

## what it does

tl;dr: paste this .mdc into .cursor/rules/ to keep Cursor from inventing global stores, mutating nested state, or stuffing server data into Zustand.

What the upstream rule covers

Sourced verbatim from PatrickJS/awesome-cursorrules → react-zustand-cursorrules-prompt-file.mdc. The rule is scoped to React + TypeScript + Zustand and is the canonical “don’t misuse Zustand” guardrail for that stack.

The state-ownership boundary the rule draws: useState/useReducer for ephemeral UI, URL state for shareable filters and pagination, Zustand only for client state genuinely shared across unrelated components. Server state goes to TanStack Query / SWR / RTK Query — never duplicated into a Zustand store “for convenience.”

What it tells Cursor

  • Store design: state + named actions per store; small, domain-focused stores (auth session, command palette, cart draft, editor); typed slices composed at the store boundary; serializable data only.
  • Component usage: smallest selector slice via useStore((s) => s.value); useShallow for object/tuple selectors; never call a store hook without a selector unless the component truly needs every field.
  • Updates and middleware: functional set((state) => nextState); immer middleware only when nested updates are materially simpler; persist only for state that must survive reloads — never tokens, refresh tokens, raw PII, or long-lived auth.
  • TypeScript: explicit state and action interfaces; no any; discriminated unions for complex local status; export store types when tests or vanilla factories need them.
  • SSR / RSC: never read or mutate browser-only stores from React Server Components; per-request vanilla stores for server-initialized state; guard persisted stores against hydration mismatches.
  • Testing: test store actions directly without rendering React; reset to initial state between tests; mock server-state libraries instead of routing fetched data through Zustand.
  • Anti-patterns the rule blocks: one global app store; form input state in Zustand unless multiple distant components edit the same draft; direct nested mutation without Immer; Zustand as an event bus; Redux-style reducers, action constants, and dispatch wrappers.

Install

mkdir -p .cursor/rules
curl -L -o .cursor/rules/react-zustand.mdc \
  https://raw.githubusercontent.com/PatrickJS/awesome-cursorrules/main/rules/react-zustand-cursorrules-prompt-file.mdc

Minimal store shape the rule enforces

import { create } from "zustand"

interface SidebarState {
  isOpen: boolean
  activePanelId: string | null
}

interface SidebarActions {
  openPanel: (panelId: string) => void
  close: () => void
  toggle: () => void
}

type SidebarStore = SidebarState & SidebarActions

export const useSidebarStore = create<SidebarStore>()((set) => ({
  isOpen: false,
  activePanelId: null,
  openPanel: (panelId) => set({ isOpen: true, activePanelId: panelId }),
  close: () => set({ isOpen: false, activePanelId: null }),
  toggle: () => set((state) => ({ isOpen: !state.isOpen })),
}))

Gotchas

  • The rule’s persistence guard is the load-bearing one: Cursor will happily persist whatever you put in persist unless you tell it not to. Keep tokens and PII out of browser storage.
  • useShallow is the rule’s answer to the “subscribed to the whole store, re-rendering on every change” footgun. Use it for any multi-field selector.
  • The globs: **/*.ts, **/*.tsx, alwaysApply: false frontmatter in the upstream .mdc means Cursor applies this only when the rule is referenced or the file matches — paste it as-is, do not flip alwaysApply to true unless you want Zustand opinions on every CSS edit.

## embed this badge

cache ✓ in cache.directory
![cached](https://cache.directory/badge/react-zustand.svg)