Cursor Rules for Zustand: Stores, Selectors, and Persistence
A practical Cursor rules template for Zustand projects: store ownership, selectors, persistence, and tests, with fenbs preserving the state-management decisions your assistant needs before changing a store.
Updated 4 min read
Cursor rules for Zustand tell an assistant how your stores are named, who owns each slice, how selectors should be written, and what a correct persistence setup looks like. Without them, an assistant will write working Zustand code that quietly breaks your conventions: a new store added to the wrong file, a selector that re-renders on every action, or a persisted key that collides with an existing one. fenbs keeps the state-management decisions your team has settled, so the next session reads them before touching a store.
This guide provides a template for each area and explains the traps that state-management rules most often prevent.
What Zustand-specific rules need to cover
A plain coding assistant knows the Zustand API. It does not know your team's decisions: whether each feature owns its store, whether selectors live in the store file or alongside components, or which slices are persisted and which are not. Rules fill that gap. They are most useful when they prevent the change that seemed correct and broke something subtle, like a selector that looks fine but reads the entire store object and triggers unnecessary renders.
The best time to write Zustand rules is before a new feature is added, not after the first performance issue appears. A rule that says "create a new store in src/stores/ for each feature domain" costs nothing when the project has two stores. It prevents a real refactor when it has twelve. Start with the store ownership and selector rules, and add the persistence and test rules as those areas become relevant to active work.
- Store ownership: one store per feature or domain, not one global store with everything in it.
- Selector conventions: how to write a selector that does not cause extra renders.
- Persistence: which slice keys are persisted, the storage backend, and versioning.
- Actions: where actions live (inside the store, outside, or in a separate slice) and how they are named.
- Tests: how to test a store without mounting a component.
Store ownership and structure rules
The most common Zustand mistake at scale is letting stores grow until one slice knows too much about another. Rules that define store boundaries prevent this. If the payment feature owns its own store, an assistant adding a cart feature does not reach into the payment store; it creates or extends the cart store.
# Zustand store conventions ## One store per feature domain Each feature (cart, user, payment, ui) has its own store file in src/stores/<feature>.ts. Do not add slices from multiple features to one store. Do not import one feature store inside another. Use event callbacks or context at the component level. ## Naming Store hook: use<Feature>Store (useCartStore, useUserStore). Store file: src/stores/<feature>.ts. State interface: <Feature>State (CartState, UserState). ## Actions inside the store Define actions as part of the store using set and get. Do not define actions outside the create call. Name actions as verbs: addItem, removeItem, clearCart. Do not prefix with on or handle. ## Avoid merging Immer unless the project already uses it Do not introduce immer middleware unless src/stores/ already has it in use.
Selector rules
A Zustand selector that returns a new object on every read causes the component using it to render on every store update, even unrelated ones. This is the most common performance issue in Zustand projects and the one an assistant without context is most likely to introduce. The rule needs to be specific: name the pattern to avoid, name the pattern to use.
# Zustand selectors
## Prefer primitive selectors
Select individual primitives rather than objects:
// Good
const count = useCartStore((s) => s.itemCount);
const userId = useUserStore((s) => s.userId);
// Avoid — new object on every render
const { itemCount, total } = useCartStore((s) => ({ itemCount: s.itemCount, total: s.total }));
## Multiple values: use shallow
When a component needs more than one value from a store, use the shallow equality function:
import { useShallow } from 'zustand/react/shallow';
const { itemCount, total } = useCartStore(useShallow((s) => ({ itemCount: s.itemCount, total: s.total })));
## Derived values
Compute derived values inside the selector, not in the component. This keeps components lean
and makes the derivation easy to test in isolation.Persistence and hydration rules
Persisted stores carry extra risks: a key collision corrupts another store's state, and a schema change without a version bump leaves users with stale data that the app cannot read. Rules in this area are essentially a checklist an assistant must work through before writing a persist call.
# Zustand persistence
## When to persist
Only persist stores explicitly listed in docs/state-persistence.md.
Do not add persistence to a new store without recording the decision.
## Key naming
Persistence key format: <app-prefix>-<feature>-store (e.g. myapp-cart-store).
Do not reuse a key that is already in use elsewhere in the codebase.
## Versioning
Every persisted store must set version and a migrate function.
Increment the version whenever the persisted shape changes.
persist(
(set) => initialState,
{
name: 'myapp-cart-store',
version: 1,
migrate: (persisted, version) => { ... },
}
)
## Hydration
For SSR projects, wrap the store provider in a Suspense boundary or use onRehydrateStorage
to avoid hydration mismatches.Test conventions for Zustand stores
Zustand stores can be tested without mounting a component, which makes store tests fast and isolated. An assistant without a rule will often reach for a render-based test instead. A rule that shows the preferred pattern keeps the test suite consistent. Direct store testing also makes edge cases practical to cover: an action that handles a network error, an action called twice in sequence, or a persisted store receiving a migration. Write the test rule to include a complete example that creates the store, calls an action, and asserts the state. An example in the rule is more useful than a description of the pattern. It also makes it easier to spot when an assistant has drifted: a test that mounts a full component where a direct store test was expected is immediately visible as wrong, which is not the case when the rule only describes the approach in words.
# Zustand store tests
## Test the store directly, not through components
Import the store creator and test actions directly without rendering:
import { createCartStore } from '../src/stores/cart';
it('adds an item', () => {
const store = createCartStore();
store.getState().addItem({ id: '1', price: 10 });
expect(store.getState().itemCount).toBe(1);
});
## Reset between tests
Each test should start from a fresh store. Export a resetStore action or use
beforeEach(() => useCartStore.setState(initialState));
## Do not test selector implementation
Test what a selector returns for a given state. Do not test that it calls useShallow.Keep your state-management decisions where your AI finds them
fenbs stores Skills and prompts alongside your project decisions. Add the agreed selector pattern as a skill, and record the decision about which stores are persisted as a standing rule. When you start a new Cursor session on the frontend, ask it to read the project context first. It finds the current state-management rules before it opens a store file.
Create a free accountFree for one person · No credit card · Works with Claude, ChatGPT, Cursor and more
How to do this with fenbs
- Create a free account and add a project for your frontend codebase.
- Record the store ownership decision in fenbs as a Decision: which features own which stores, and whether the project uses a global store or feature stores. Include the reason.
- Add the selector pattern your team agreed on as a skill in fenbs. Name it something like "Zustand selector pattern". Any connected assistant can list it and read the full rule before writing a selector.
- Connect Cursor to fenbs over MCP from Integrations. Ask Cursor to read the project context before working on a store.
- When you change the persistence schema for a store, update the version in code and record the change in fenbs. The next assistant sees the current version and the reason for the last migration.
For rules that apply across multiple frontend projects, the Cursor rules library guide explains how to share rule files without duplicating them.