React AI Coding Rules
React's flexible component model is both its superpower and its biggest trap for AI-generated code. Without clear rules, AI assistants produce bloated components, misuse `useEffect`, ignore performance optimisations, and scatter state management logic. React AI rules define component size limits, hook usage patterns, state colocation principles, and rendering best practices so that Cursor, Windsurf, and Copilot generate components that are readable, testable, and performant from day one. The rules collected on AI Rules Hub cover function component patterns, Context API usage, React Query conventions, Suspense boundaries, and accessibility requirements for modern React codebases.
Why Use AI Rules for React?
- Stop AI from creating class components or outdated lifecycle patterns
- Enforce single-responsibility components with a clear prop interface
- Prevent unnecessary `useEffect` calls for derived state or data fetching
- Standardise hook naming (`use` prefix) and placement rules
- Ensure accessible markup (ARIA labels, semantic HTML) in all generated components
Best Practices for React AI Coding
One Component Per File
Instruct AI to keep each component in its own file, co-located with its styles and tests, making the codebase easy to navigate.
Derive State, Don't Duplicate It
Rule AI assistants to compute values from existing state using `useMemo` rather than introducing duplicate state variables that can drift out of sync.
Explicit Prop Types
Require explicit TypeScript interfaces for every component's props — no implicit `any`, no spreading unknown objects into JSX.
Semantic HTML in Components
Enforce the use of `<article>`, `<section>`, `<nav>`, and `<button>` over generic `<div>` elements to produce accessible, SEO-friendly markup.
Common Patterns & Standards
Container / Presentational Split
Separate data-fetching logic (containers) from display logic (presentational components) to keep components testable and reusable.
Custom Hooks for Logic
Extract non-trivial state and effects into named custom hooks (`useUserProfile`, `useRuleList`) rather than keeping logic inline in components.
Compound Components Pattern
For complex UI elements like modals or tabs, use compound component patterns with Context to share state without prop drilling.
Early Returns Over Nested Conditionals
Instruct AI to use early returns for loading/error states rather than deeply nested ternaries inside JSX.
Top React Rules on AI Rules Hub
No description provided.
## **Core Principles**
* Use server-first architecture; prefer server components unless client-side behavior is required.
* Maintain strict separation of concerns across UI, business logic, and data layers.
* Ensure all code is scalable, modular, and maintainable.
---
## **Data Fetching Rules**
* Do not call `fetch()` directly inside components.
* All API communication must be handled through a centralized HTTP client.
* API logic must exist only in the `services` layer.
* UI components must access data only through hooks.
---
## **Component Rules**
* Each component must have a single responsibility.
* Component files must not exceed 150 lines.
* Target component size should be between 50–80 lines.
* Large components must be split into smaller reusable components.
---
## **State Management Rules**
* Use a centralized state management solution (e.g., Zustand or Redux Toolkit).
* Do not use `useContext` for global state management.
* Keep global state minimal and domain-specific.
---
## **Forms & Validation Rules**
* All forms must use a structured form library (e.g., React Hook Form).
* All validation must be schema-based (e.g., Zod).
* Validation schemas must be reusable and stored separately.
---
## **TypeScript Rules**
* Strict TypeScript is mandatory.
* Do not use `any` type.
* All API responses must use a generic structure (e.g., `ApiResponse<T>`).
* Types must be defined and reused across the application.
---
## **Routing Rules**
* All routes must be centralized in a constants file.
* Do not hardcode route paths inside components or navigation logic.
---
## **Enums & Constants Rules**
* Do not hardcode strings such as roles, statuses, or API identifiers.
* All enums must be defined in a centralized location.
* Reuse enums across all features.
---
## **Directory Structure Rules**
* Follow feature-based architecture:
```
src/
features/
[feature]/
components/
services/
hooks/
types/
schemas/
components/ui/
store/
lib/
constants/
types/enums/
```
---
## **Layer Responsibilities**
* UI layer must handle rendering only.
* Hooks must handle business logic and data orchestration.
* Services must handle API communication.
* Store must handle global state only.
* Schemas must handle validation.
* Types must define contracts and interfaces.
---
## **Prohibited Practices**
* Do not call APIs directly inside UI components.
* Do not use `fetch()` directly.
* Do not hardcode routes or static strings.
* Do not create large monolithic components.
* Do not use `any` in TypeScript.
---
## **Architecture Philosophy**
* Build systems, not pages.
* Prioritize consistency over flexibility.
* Ensure every module is reusable and replaceable.
* Maintain clear boundaries between layers.
---
## **Compatibility**
* This rule set is designed for modern frontend stacks (React, Next.js, TanStack, etc.).
* Aligns with standardized rule-sharing ecosystems like Airuleshub .No description provided.
# Pure State — Zustand Patterns That Actually Scale
> **AI Instruction**: This document defines the rules, conventions, and architecture decisions for implementing Zustand state management in a React + TypeScript project. Do not deviate from these patterns. When generating stores, hooks, or components — always refer back to these rules first.
---
## Stack
- **Zustand v4+** — primary state management
- **TypeScript strict mode** — all stores must be fully typed
- **Immer middleware** — for any nested state mutations
- **DevTools middleware** — applied to every store, always
- **Persist middleware** — only for state that must survive page refresh
---
## Folder Structure
```
src/
├── stores/
│ ├── index.ts # Re-export all stores + resetAllStores()
│ ├── useAuthStore.ts
│ ├── useUIStore.ts
│ ├── useUserStore.ts
│ └── slices/ # Only for large, multi-concern stores
│ ├── types.ts
│ ├── authSlice.ts
│ └── cartSlice.ts
├── hooks/
│ └── useStoreSelectors.ts # All memoized selectors live here
└── types/
└── store.d.ts # Shared store type declarations
```
---
## Store Architecture Rules
### One Store Per Domain
Every store must own exactly one concern. Never mix auth logic with UI state, or user profile with cart data. If two pieces of state are unrelated, they belong in separate stores. The store name must clearly reflect what it owns — `useAuthStore`, `useCartStore`, `useUIStore`.
### Always Define an Initial State Object
Every store must declare a separate `initialState` const above the store definition. This object is what the `reset()` action returns to. It also documents at a glance what shape the store holds without reading the full type definition.
### Every Store Must Have a Reset Action
Without exception, every store must implement a `reset()` action that restores the store to its `initialState`. This is called on logout, session expiry, or any global teardown. A store without `reset()` is incomplete.
### Always Apply DevTools Middleware
Every store — no matter how small — must be wrapped with `devtools()` middleware. The store name must be passed as the `name` option. Every `set()` call must include a descriptive action name as the third argument so that DevTools traces are meaningful and debuggable.
### Actions Belong Inside the Store
No business logic or state mutation should live outside the store. Components call store actions — they never call `set()` directly or manage async themselves. If a component is doing `setState` based on API data, that logic needs to move into a store action.
---
## TypeScript Conventions
### Always Separate State from Actions in the Interface
Define the store interface with a clear visual separation — state fields first, then actions. Use comments to divide them. This makes the interface scannable and prevents confusion between what is data and what is callable.
### Use Strict Typing for All State Fields
Never use `any`. Every field must have an explicit type. Optional fields must be typed as `T | null` — never as `T | undefined` unless the field is genuinely optional in a form or partial update context. Null signals "not yet loaded". Undefined signals "doesn't exist".
### Infer Action Parameter Types from Zod Schemas When Available
If the project uses Zod for API validation, action input types should be inferred from those schemas using `z.infer<typeof schema>`. Never duplicate type definitions between Zod schemas and store interfaces.
---
## Async Action Rules
### Every Async Action Must Follow the Three-Phase Pattern
When an async action starts, immediately set `isLoading: true` and `error: null`. On success, set the result data and `isLoading: false`. On failure, set the error message and `isLoading: false`. No async action is complete without all three phases handled.
### Always Name Async Action States in DevTools
When calling `set()` inside an async action, name each phase clearly — for example `login/pending`, `login/fulfilled`, `login/rejected`. This mirrors Redux Toolkit conventions and makes DevTools traces readable.
### Never Let Errors Silently Fail
Every catch block must write to the store's `error` field. The error message must be a human-readable string. Always check if the caught value is an instance of `Error` before accessing `.message` — otherwise fall back to a generic string.
### Access Current State Inside Async Actions with `get()`
When an async action needs to read current state mid-execution — for example to attach a token to a request — use `get()` from the store creator. Never close over state values from outside the action, as they may be stale.
### Never Trigger Actions from Inside Other Actions
Actions must not call sibling actions directly. If two actions share logic, extract that logic into a private utility function inside the store file and call it from both. Cross-action dependencies cause unpredictable execution order.
---
## Selector Rules
### Never Subscribe to the Whole Store
Components must never destructure the full store object. Subscribing to the whole store causes the component to re-render on every single state change regardless of relevance. Always pass a selector function that picks exactly the value the component needs.
### All Selectors Live in `useStoreSelectors.ts`
Every named selector — `useIsAuthenticated`, `useCurrentUser`, `useAuthLoading` — must be defined in the dedicated selectors file and exported from there. Components import from this file, not directly from the store. This centralizes all selector logic and makes refactoring easier.
### Actions Are Safe to Destructure Together
Action functions are stable references and do not cause re-renders. It is acceptable to select a group of actions together in one selector. State values must always be selected individually.
### Use Shallow Comparison for Object Selections
If a selector must return an object with multiple fields, use `shallow` from Zustand as the equality function. Without this, the component re-renders even when the object's values haven't changed because a new object reference is returned on every call.
---
## Immer Middleware Rules
### Use Immer Only When State is Nested Two or More Levels Deep
For flat state with primitive values, use regular `set()`. Apply Immer middleware to stores where deeply nested objects need to be updated — user profiles with nested preferences, addresses, or settings. Immer is not needed for simple top-level assignments.
### Never Return and Mutate in the Same Immer Callback
Inside an Immer `set()` callback, either mutate the draft directly or return a new object — never both. Mixing the two causes Immer to throw. Choose mutation style for nested updates, return style for complete state replacements.
### Always Guard Against Null Before Mutating Nested Objects
Before mutating a nested object inside an Immer callback, always check that the parent exists. If the parent is null and the action tries to mutate a child property, Immer will throw a runtime error. Defensive null checks are mandatory.
---
## Persist Middleware Rules
### Only Persist What Is Truly Necessary
Never persist entire store state by default. Always use `partialize` to explicitly whitelist fields that should survive a page refresh. Persisting loading states, error messages, or transient UI values is a bug.
### Always Name the Storage Key Explicitly
The `name` field in persist config is the localStorage key. It must be explicit, unique, and human-readable — for example `auth-storage` or `theme-preferences`. Never use auto-generated or ambiguous names.
### Persist Tokens, Not Sensitive Data
Authentication tokens may be persisted in localStorage for session continuity. Passwords, raw API responses, or personally identifiable information must never be persisted. When in doubt, do not persist.
---
## Slice Pattern Rules
### Use Slices Only for Large Multi-Domain Stores
The slice pattern — combining multiple `StateCreator` functions into one bound store — is appropriate when a single store legitimately crosses domains, such as a checkout flow that needs both cart state and auth state together. For typical apps, separate stores are preferred over slices.
### Define All Slice Types in a Shared Types File
Every slice interface must be defined in `slices/types.ts`. The combined `AppStore` type that merges all slice interfaces also lives there. Individual slice files import from types — they never redefine their own interfaces inline.
### Each Slice Is Responsible Only for Its Own State
A slice action must never directly mutate another slice's state. If coordination is needed between slices, it must happen at the bound store level through composition, not by cross-referencing slice internals.
---
## Reset and Cleanup Rules
### Export a `resetAllStores()` Function from `stores/index.ts`
The `index.ts` file must export a single `resetAllStores()` function that calls `.getState().reset()` on every store. This function is called in exactly one place — the logout handler. It is never called from components directly.
### Subscriptions Created Outside React Must Be Unsubscribed
If `store.subscribe()` is used outside a React component — for example to attach an auth token to an API client — the returned unsubscribe function must be stored and called during application teardown. Leaking subscriptions causes stale callbacks.
---
## Component Integration Rules
### Components Are Consumers Only
A component's only job with respect to state is to read values via selectors and call actions on user interaction. Components must not derive new state, filter arrays, or compute totals inline. That logic belongs in the store or in a dedicated selector.
### Co-locate Loading and Error State with Their Data
Whenever a component renders async data, it must also handle the corresponding `isLoading` and `error` states from the same store. Rendering data without handling its loading and error states is incomplete.
### Clear Errors Explicitly
After an async action fails and the error is displayed, always provide a mechanism — a dismiss button, a timeout, or a navigation event — to call `clearError()`. Errors must not persist in state indefinitely.
---
## Anti-Patterns — Never Do These
| ❌ Never | ✅ Always |
|---|---|
| Subscribe to the full store object | Select only the value needed |
| Put async logic inside components | Move async into store actions |
| Mutate state directly | Use `set()` or Immer |
| Create a store without DevTools | Every store gets `devtools()` |
| Skip loading or error state | All three async phases are required |
| Mix UI state and domain state | Separate stores per concern |
| Persist loading, errors, or UI flags | Only persist tokens and preferences |
| Call `reset()` from components | Use `resetAllStores()` from logout only |
| Duplicate types already in Zod schemas | Infer with `z.infer<>` |
| Hardcode localStorage key strings | Define as named constants |
---
## Final Checklist for AI Initialization
- [ ] One store file created per domain concern
- [ ] Every store has `initialState` const defined above the store
- [ ] Every store has a `reset()` action returning `initialState`
- [ ] Every store is wrapped with `devtools()` middleware and named
- [ ] All async actions handle `pending`, `fulfilled`, and `rejected` phases
- [ ] All selectors are defined in `useStoreSelectors.ts`
- [ ] No component subscribes to the full store object
- [ ] `resetAllStores()` is exported from `stores/index.ts`
- [ ] Persist middleware uses `partialize` to whitelist only necessary fields
- [ ] Immer middleware applied only to stores with nested state
- [ ] All store interfaces separate state fields from action fields
- [ ] No async logic lives inside React componentsBest practices for managing application state in React using Redux Toolkit. This rule promotes using createSlice, createAsyncThunk, typed hooks, and a feature-based structure to build scalable and ma
Use Redux Toolkit as the standard state management solution for React applications. Always create Redux logic using createSlice instead of manually writing reducers and action types. Structure Redux features using a feature-based folder structure where each slice contains its reducer, actions, and selectors. Use createAsyncThunk for handling asynchronous logic such as API calls. Keep Redux state minimal and avoid storing derived values that can be calculated from existing state. Prefer using the Redux Toolkit configureStore function to automatically enable useful middleware and dev tools. Avoid writing mutable logic in reducers unless using Redux Toolkit's Immer-powered syntax. Use typed hooks in TypeScript projects: - useAppDispatch - useAppSelector Do not store UI-only state (modals, form inputs, temporary values) in Redux unless it needs to be shared globally. Place all slices inside a dedicated "store" or "features" directory. Use selectors to access Redux state instead of directly referencing state structure in components. Ensure slices remain small and focused on a single domain or feature.
Best practices to improve web performance and Lighthouse score.
When writing frontend code, follow these performance rules: 1. Reduce JavaScript bundle size. 2. Lazy load components when possible. 3. Use code splitting. 4. Avoid unnecessary state updates. 5. Debounce expensive operations. 6. Optimize images and use modern formats (WebP). 7. Avoid blocking scripts. 8. Use memoization for heavy computations. 9. Reduce DOM nodes where possible. 10. Ensure Lighthouse performance score stays above 90.
Rules to ensure scalable, performant, and maintainable Next.js applications.
You are an expert Next.js developer. Follow these rules when generating or modifying code: 1. Always prefer Server Components in Next.js unless client-side interactivity is required. 2. Use the App Router structure and organize files by feature: /app /components /lib /hooks /services 3. Avoid unnecessary client-side JavaScript. Use "use client" only when required. 4. Optimize images using next/image. 5. Use dynamic imports for heavy components to reduce bundle size. 6. Always implement loading.tsx and error.tsx for routes. 7. Follow SEO best practices using metadata API. 8. Avoid inline styles; prefer Tailwind or CSS modules. 9. Ensure accessibility (aria labels, semantic HTML). 10. Optimize performance by reducing main-thread work and avoiding heavy libraries. Always prioritize performance, readability, and scalability.
These rules enforce enterprise-level type safety, maintainability, and predictable React architecture.
## 1️⃣ Strict tsconfig.json
``` json
{
"compilerOptions": {
"target": "ES2022",
"lib": ["DOM", "DOM.Iterable", "ES2022"],
"module": "ESNext",
"jsx": "react-jsx",
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitOverride": true,
"noFallthroughCasesInSwitch": true,
"noPropertyAccessFromIndexSignature": true,
"useUnknownInCatchVariables": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"skipLibCheck": false
}
}
```
------------------------------------------------------------------------
## 2️⃣ Never Use `any`
Use `unknown` and narrow types safely.
------------------------------------------------------------------------
## 3️⃣ Always Type Component Props
``` tsx
interface ButtonProps {
label: string;
onClick: () => void;
}
export function Button({ label, onClick }: ButtonProps): JSX.Element {
return <button onClick={onClick}>{label}</button>;
}
```
------------------------------------------------------------------------
## 4️⃣ Always Define Component Return Types
``` tsx
export function Header(): JSX.Element {
return <header>App</header>;
}
```
------------------------------------------------------------------------
## 5️⃣ Do NOT Use `React.FC`
Prefer explicit function components with typed props.
------------------------------------------------------------------------
## 6️⃣ Properly Type useState
``` tsx
interface User {
id: string;
name: string;
}
const [user, setUser] = useState<User | null>(null);
```
------------------------------------------------------------------------
## 7️⃣ Strict Event Typing
``` tsx
const handleChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
setValue(e.target.value);
};
```
------------------------------------------------------------------------
## 8️⃣ Use Discriminated Unions for State
``` tsx
type FetchState =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: string }
| { status: "error"; error: Error };
```
------------------------------------------------------------------------
## 9️⃣ Never Use Non-Null Assertion (`!`)
Always check for null explicitly.
------------------------------------------------------------------------
## 🔟 Always Type useRef
``` tsx
const inputRef = useRef<HTMLInputElement | null>(null);
```
------------------------------------------------------------------------
## 1️⃣1️⃣ Strict Context Typing
``` tsx
interface AuthContextType {
user: User | null;
login: (email: string) => Promise<void>;
}
```
------------------------------------------------------------------------
## 🏆 Golden Enterprise Rules
- ❌ No `any`
- ❌ No implicit return types
- ❌ No `React.FC`
- ❌ No non-null assertion
- ❌ No unsafe casting
- ✅ Discriminated unions
- ✅ Explicit typing everywhere
- ✅ Strict ESLint + tsconfigThis guide provides a production-ready architectural pattern for implementing TanStack Query in a React application. It focuses on scalability, type safety, and maintainability.
# The Guide to TanStack Query (React Query) in React + Vite + TypeScript
This guide provides a production-ready architectural pattern for implementing TanStack Query in a React application. It focuses on scalability, type safety, and maintainability.
## 1. Installation & Setup
First, install the core package and the devtools.
```bash
npm install @tanstack/react-query @tanstack/react-query-devtools axios
```
### Global Configuration (`src/main.tsx`)
Wrap your application in `QueryClientProvider`.
```tsx
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import App from './App';
// Create a client
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // Data is fresh for 5 minutes
gcTime: 1000 * 60 * 60, // Garbage collect unused data after 1 hour
retry: 1, // Retry failed requests once
refetchOnWindowFocus: false, // Prevent refetching on window focus (optional)
},
},
});
createRoot(document.getElementById('root')!).render(
<StrictMode>
<QueryClientProvider client={queryClient}>
<App />
{/* DevTools are essential for debugging */}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
</StrictMode>
);
```
---
## 2. Recommended Directory Structure
Isolate your data fetching logic from your UI components.
```
src/
├── api/ # Pure Axios/Fetch functions
│ ├── axios-client.ts # configured axios instance
│ └── todos.api.ts # API methods for a specific feature
├── hooks/ # Custom React Query hooks
│ └── queries/ # Group hooks by feature
│ └── useTodos.ts
├── types/ # TypeScript interfaces
│ └── todo.types.ts
└── lib/
└── query-keys.ts # Centralized query keys
```
---
## 3. Query Key Factory Pattern (`src/lib/query-keys.ts`)
**Crucial:** Never hardcode query keys (strings/arrays) directly in your components. Use a factory to maintain consistency.
```typescript
// src/lib/query-keys.ts
export const todoKeys = {
all: ['todos'] as const,
lists: () => [...todoKeys.all, 'list'] as const,
list: (filters: string) => [...todoKeys.lists(), { filters }] as const,
details: () => [...todoKeys.all, 'detail'] as const,
detail: (id: number) => [...todoKeys.details(), id] as const,
};
```
---
## 4. The API Layer (`src/api/*`)
Keep API calls strictly as pure functions returning Promises. They should know nothing about React.
```typescript
// src/api/todos.api.ts
import axios from 'axios';
import { Todo } from '../types/todo.types';
const api = axios.create({ baseURL: 'https://jsonplaceholder.typicode.com' });
export const fetchTodos = async (): Promise<Todo[]> => {
const { data } = await api.get('/todos');
return data;
};
export const fetchTodoById = async (id: number): Promise<Todo> => {
const { data } = await api.get(`/todos/${id}`);
return data;
};
export const createTodo = async (newTodo: Omit<Todo, 'id'>): Promise<Todo> => {
const { data } = await api.post('/todos', newTodo);
return data;
};
```
---
## 5. Custom Hooks (The "Glue" Layer)
Wrap `useQuery` and `useMutation` in custom hooks. This gives you a single place to handle key management, types, and default options.
### Fetching Data (`useQuery`)
```typescript
// src/hooks/queries/useTodos.ts
import { useQuery } from '@tanstack/react-query';
import { todoKeys } from '../../lib/query-keys';
import { fetchTodos, fetchTodoById } from '../../api/todos.api';
export const useTodos = () => {
return useQuery({
queryKey: todoKeys.lists(),
queryFn: fetchTodos,
staleTime: 1000 * 60, // 1 minute specific stale time for this query
});
};
export const useTodo = (id: number) => {
return useQuery({
queryKey: todoKeys.detail(id),
queryFn: () => fetchTodoById(id),
enabled: !!id, // Only run if ID is present
});
};
```
### Mutating Data (`useMutation`)
Always invalidate related queries on success to keep the UI in sync.
```typescript
// src/hooks/queries/useTodos.ts
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { createTodo } from '../../api/todos.api';
import { todoKeys } from '../../lib/query-keys';
export const useCreateTodo = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: createTodo,
onSuccess: () => {
// Invalidate the 'lists' query so the new item appears
queryClient.invalidateQueries({ queryKey: todoKeys.lists() });
},
onError: (error) => {
console.error('Failed to create todo:', error);
},
});
};
```
---
## 6. Usage in Components
Components become clean and focused purely on UI logic.
```tsx
// src/components/TodoList.tsx
import { useTodos, useCreateTodo } from '../hooks/queries/useTodos';
export const TodoList = () => {
const { data: todos, isLoading, isError } = useTodos();
const createMutation = useCreateTodo();
if (isLoading) return <div>Loading...</div>;
if (isError) return <div>Error fetching todos</div>;
const handleCreate = () => {
createMutation.mutate({ title: 'New Task', completed: false, userId: 1 });
};
return (
<div>
<button onClick={handleCreate} disabled={createMutation.isPending}>
{createMutation.isPending ? 'Adding...' : 'Add Todo'}
</button>
<ul>
{todos?.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
</div>
);
};
```
---
## 7. Advanced Patterns
### Optimistic Updates
Update the UI _immediately_ before the server responds.
```typescript
export const useUpdateTodo = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: updateTodoApi,
onMutate: async (newTodo) => {
// 1. Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: todoKeys.detail(newTodo.id) });
// 2. Snapshot previous value
const previousTodo = queryClient.getQueryData(todoKeys.detail(newTodo.id));
// 3. Optimistically update
queryClient.setQueryData(todoKeys.detail(newTodo.id), (old: any) => ({
...old,
...newTodo,
}));
// 4. Return context
return { previousTodo };
},
onError: (err, newTodo, context) => {
// 5. Rollback on error
queryClient.setQueryData(todoKeys.detail(newTodo.id), context?.previousTodo);
},
onSettled: (newTodo) => {
// 6. Refetch to ensure server sync
queryClient.invalidateQueries({ queryKey: todoKeys.detail(newTodo?.id) });
},
});
};
```
### Type-Safe Error Handling
Define a global error type for your API.
```typescript
// types/api.types.ts
export interface ApiError {
message: string;
statusCode: number;
}
// In custom hook
import { useQuery, UseQueryOptions } from '@tanstack/react-query';
import { ApiError } from '../types/api.types';
export const useTodos = (options?: UseQueryOptions<Todo[], ApiError>) => {
return useQuery({
queryKey: todoKeys.lists(),
queryFn: fetchTodos,
...options,
});
};
```
---
## 8. Best Practices Checklist
- [ ] **Global Stale Time**: Set a reasonable global `staleTime` (e.g., 5 mins) to avoid excessive background fetching.
- [ ] **Query Key Factories**: Always use a factory/object for keys (`todoKeys.list(filter)`).
- [ ] **Separation of Concerns**: `Component` -> `Custom Hook` -> `API Function`.
- [ ] **Invalidation**: Always invalidate parent lists when creating/deleting items.
- [ ] **DevTools**: Keep `ReactQueryDevtools` in your app wrapper (it's stripped in production).Explore Related AI Rules
Share Your React AI Rules
Have rules that improved your React workflow? Submit them to AI Rules Hub and help the community get better results from AI coding assistants.
