Comprehensive Rules for designing, implementing, and maintaining predictable, performant state layers in React/TypeScript applications.
The endless state management debates are over. These battle-tested Cursor Rules deliver a clear framework for building scalable, performant React applications with TypeScript—no more architectural paralysis or performance surprises.
You've been there: components re-rendering unnecessarily, server data scattered across client stores, and debugging sessions that stretch into the night. The React ecosystem offers powerful state management options, but without clear guidelines, you end up with:
These Cursor Rules establish a comprehensive framework that treats state management as what it actually is: the foundation of your application's reliability and performance. Instead of defaulting to complex solutions, you'll follow a clear escalation path from local state to global stores, keeping your architecture clean and your bundle lean.
The Framework Approach:
Performance Gains You Can Measure:
Development Velocity Improvements:
Code Quality Enhancements:
// Scattered state everywhere
const UserProfile = () => {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
// Duplicated server calls
useEffect(() => {
fetchUser().then(setUser).catch(setError);
}, []);
// No error handling, manual cache management
return loading ? <Spinner /> : <UserCard user={user} />;
};
// Server state with React Query
const useUser = (userId: string) => {
return useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
staleTime: 5 * 60 * 1000, // 5 minutes
});
};
// Client state with Zustand (when needed)
const useCartStore = create<CartState>()((set) => ({
items: [],
addItem: (item) => set((state) => ({
items: [...state.items, item]
})),
}));
// Clean component
const UserProfile = ({ userId }: { userId: string }) => {
const { data: user, isLoading, error } = useUser(userId);
const cartTotal = useCartStore(state => state.total);
if (isLoading) return <Spinner />;
if (error) return <ErrorMessage error={error} />;
return <UserCard user={user} cartTotal={cartTotal} />;
};
// features/cart/cartSlice.ts
export interface CartState {
items: CartItem[];
total: number;
status: 'idle' | 'loading' | 'failed';
}
const initialState: CartState = {
items: [],
total: 0,
status: 'idle',
};
const cartSlice = createSlice({
name: 'cart',
initialState,
reducers: {
addItem: (state, action: PayloadAction<CartItem>) => {
state.items.push(action.payload);
state.total += action.payload.price;
},
removeItem: (state, action: PayloadAction<string>) => {
const index = state.items.findIndex(item => item.id === action.payload);
if (index !== -1) {
state.total -= state.items[index].price;
state.items.splice(index, 1);
}
},
},
extraReducers: (builder) => {
builder
.addCase(submitOrder.pending, (state) => {
state.status = 'loading';
})
.addCase(submitOrder.fulfilled, (state) => {
state.items = [];
state.total = 0;
state.status = 'idle';
});
},
});
// Memoized selectors
export const selectCartItems = (state: RootState) => state.cart.items;
export const selectCartTotal = createSelector(
[selectCartItems],
(items) => items.reduce((sum, item) => sum + item.price, 0)
);
export const { addItem, removeItem } = cartSlice.actions;
export { initialState }; // For testing
export default cartSlice.reducer;
# Install dependencies
npm install @reduxjs/toolkit react-redux zustand jotai @tanstack/react-query
# Create folder structure
mkdir -p src/{app,features,libs/{zustand,atoms},ui/{components,hooks}}
// src/app/queryClient.ts
import { QueryClient } from '@tanstack/react-query';
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minutes
retry: (failureCount, error) => {
if (error.status === 404) return false;
return failureCount < 3;
},
},
},
});
// src/app/App.tsx
import { StrictMode } from 'react';
import { QueryClientProvider } from '@tanstack/react-query';
import { Provider } from 'react-redux';
import { store } from './store';
import { queryClient } from './queryClient';
import { ErrorBoundary } from './ErrorBoundary';
export const App = () => (
<StrictMode>
<ErrorBoundary>
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<Router>
<Routes>
{/* Your routes */}
</Routes>
</Router>
</Provider>
</QueryClientProvider>
</ErrorBoundary>
</StrictMode>
);
// src/features/auth/authSlice.ts
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
export const loginUser = createAsyncThunk(
'auth/loginUser',
async (credentials: LoginCredentials) => {
const response = await authAPI.login(credentials);
return response.data;
}
);
const authSlice = createSlice({
name: 'auth',
initialState: {
user: null,
token: null,
status: 'idle',
} as AuthState,
reducers: {
logout: (state) => {
state.user = null;
state.token = null;
},
},
extraReducers: (builder) => {
builder
.addCase(loginUser.fulfilled, (state, action) => {
state.user = action.payload.user;
state.token = action.payload.token;
state.status = 'idle';
});
},
});
export const { logout } = authSlice.actions;
export default authSlice.reducer;
// src/app/hooks.ts
import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux';
import type { RootState, AppDispatch } from './store';
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
Immediate Productivity Gains:
Long-term Architecture Benefits:
Team Collaboration Improvements:
Start building applications with state management that actually works. Your future self—and your teammates—will thank you when that critical bug fix needs to ship and your state logic is crystal clear, fully tested, and ready for production.
You are an expert in React 18, TypeScript 5, Redux Toolkit, Zustand, Jotai, React Query/TanStack Query, and modern build tooling (Vite, Webpack 5).
Key Principles
- Fit the tool to the problem: start with React local state, graduate to global store only when necessary.
- Separate concerns: keep UI (client) state and server (remote) state in distinct layers/stores.
- Single source of truth: avoid duplicating the same piece of data across multiple stores.
- Immutability first: always treat state as immutable–prefer copying over mutation, even in reducers that look mutable (e.g., Redux Toolkit’s `createSlice`).
- Predictability over cleverness: favor explicit selectors, pure reducers, and deterministic updates.
- Performance is a feature: memoize selectors, use structural sharing, and limit global subscriptions.
- Re-evaluate periodically: audit state size, re-render counts, and stale caches each sprint.
TypeScript (React)
- Strict mode must be enabled (`"strict": true` in `tsconfig.json`).
- All component files end with `.tsx`; pure hooks end with `.ts`.
- Export order inside a file: public types ➜ constants ➜ pure helpers ➜ React hooks ➜ components.
- Prefer arrow functions for components & hooks; name each after its file: `export const UserCard = () => …`.
- Enforce explicit return types on hooks: `const useAuth = (): AuthState => { … }`.
- Never disable `eslint @typescript-eslint/no-explicit-any`; create precise generics instead.
- Colocate unit tests alongside source: `MyHook.test.ts`.
- Directory names: kebab-case; React hook files start with `use-` (e.g., `use-user-preferences.ts`).
Error Handling and Validation
- Handle edge cases first (“guard clauses”) and `return` early to keep happy path last.
- Use Error Boundary wrappers around route boundaries and root provider.
- For server state (React Query):
• Use `onError` callbacks to surface toast/snackbar notifications.
• Prefer typed errors (`AxiosError<ApiError>`).
- Centralize logging: forward caught exceptions to `Sentry` in a single utility `reportError(error, info)`.
- State stores must expose an `initialState` export for deterministic resets in tests.
- When optimistic updates are required, always provide a rollback function to React Query’s `onError`.
React (Framework-Specific Rules)
- Only functional components; no classes.
- Wrap app in `<StrictMode>` and `<QueryClientProvider>` at the root.
- Global providers order: ErrorBoundary ➜ QueryClientProvider ➜ ThemeProvider ➜ Router ➜ StoreProvider(s).
- Use Context API solely for low-churn, composition-related flags (e.g., Theme, Auth). Otherwise pick a dedicated store.
Redux Toolkit Rules
- Organize by feature‐slice, not by file type: `/features/cart/cartSlice.ts`.
- Each slice exports: actions, reducer, typed selectors, and thunks (if needed).
- Use `createAsyncThunk` for network calls; handle `pending/fulfilled/rejected` in slice.
- Memoize selectors with `reselect` or `createSelector` from RTK.
- Never access store directly inside React components; always use typed hooks `useAppSelector`, `useAppDispatch`.
- Combine reducers via RTK’s `configureStore({ reducer })` once in `store.ts`.
Zustand Rules
- Define store in a single file: `export const useCartStore = create<CartState>()((set, get) => ({ … }))`.
- Use `select` argument to subscribe only to required keys: `useCartStore(state => state.total)`.
- Mutate using functional `set` to guarantee latest state.
- Reset pattern: `const reset = () => set(initialState)` – export for tests.
Jotai Rules
- Atom files end with `.atom.ts` and export exactly one default atom.
- Prefer `atomWithStorage` for persisted atoms and `atomWithQuery` for server interactions.
- Combine atoms through `selectAtom` instead of building derived React state.
React Query / TanStack Query Rules
- Single `queryClient` instance per app lifecycle.
- Use query keys as tuples: `["user", userId]` to leverage partial invalidation.
- Keep `staleTime` low for volatile data; increase for static reference data.
- Disable refetch on window focus for heavy data grids: `refetchOnWindowFocus: false`.
- Use mutation hooks with `invalidateQueries` in `onSuccess` rather than manual cache updates when possible.
Additional Sections
Testing
- Write unit tests for reducers/stores in pure Node; no JSDOM required.
- Component tests (React Testing Library) must assert rendered output, not internal state.
- Mock server state with MSW; provide realistic handlers per feature.
- Snapshot tests only for presentational components; avoid for interactive ones.
Performance
- Guard expensive selectors with `reselect`.
- Wrap components that consume global state in `React.memo` by default.
- Defer non-critical global state into `useDeferredValue` when user interaction is active.
- Split vendor‐heavy state libraries (e.g., XState) via `React.lazy`.
Security
- Never store sensitive tokens in client state (Redux, Zustand, etc.); keep them in secure HTTP-only cookies.
- Ensure selectors leak no secrets to DevTools in production: disable DevTools extension outside `NODE_ENV !== 'development'`.
Migration & Evolution
- Quarterly audit: how much of current global state could move local? Track with ESLint rule `no-useless-global-state` (custom).
- If bundle size of chosen store > 5% of main chunk, evaluate alternative (e.g., move from Redux to Zustand).
Folder Skeleton (reference)
```
src/
app/
store.ts # configureStore + root providers
queryClient.ts # TanStack Query client
features/
auth/
authSlice.ts
auth.selectors.ts
auth.api.ts
AuthPage.tsx
cart/
cartSlice.ts
cart.selectors.ts
cart.hooks.ts
libs/
zustand/
use-user-preferences.ts
atoms/
theme.atom.ts
ui/
components/
hooks/
```
End of ruleset.