React Best Practices
Keep React code boring, secure, accessible, and easy for agents to change. Use this with React Doctor before pushing or merging PRs.
Default Rules
Section titled “Default Rules”- Keep components small and named after product concepts.
- Put server data in TanStack Query or route loaders, not duplicated local state.
- Derive values during render; do not mirror props/query data into
useState+useEffect. - Use
useEffectonly for real side effects: subscriptions, imperative APIs, analytics, or external synchronization. - Move static config outside components: table columns, labels, nav items, schema-like arrays.
- Memoize only when it protects expensive work or stable props for memoized children.
- Prefer custom hooks for reusable behavior, not for hiding one-off component logic.
- Keep forms typed with schema validation and visible labels.
- Give icon-only buttons an
aria-label. - Avoid
dangerouslySetInnerHTML; if unavoidable, sanitize at the boundary and document why.
Data Fetching
Section titled “Data Fetching”Prefer these patterns:
const { data, isPending } = useQuery({ ...orpc.companies.list.queryOptions({ input: filters }),});const mutation = useMutation({ ...orpc.companies.update.mutationOptions(), onSuccess: () => queryClient.invalidateQueries({ queryKey: orpc.companies.key() }),});Avoid:
const [items, setItems] = useState<Item[]>([]);
useEffect(() => { fetchItems().then(setItems);}, []);Component Shape
Section titled “Component Shape”Good default:
const columns: ColumnDef<User>[] = [ { accessorKey: "email", header: "Email" },];
export function UsersTable({ users }: { users: User[] }) { return <DataTable columns={columns} data={users} />;}Avoid recreating static arrays or nested components every render:
export function UsersPage() { const columns = [{ accessorKey: "email", header: "Email" }]; function RowActions() { return <Button>...</Button>; } return <DataTable columns={columns} />;}Security and Accessibility
Section titled “Security and Accessibility”- No raw HTML unless sanitized.
- No secrets in client code or
VITE_PUBLIC_*values. - Validate user input on the server even when forms validate on the client.
- Use semantic elements before ARIA.
- Every form input needs a label or accessible name.
- Every destructive action needs a confirmation or undo path.
- External links should use
rel="noreferrer"when opening a new tab.
React Doctor Config Template
Section titled “React Doctor Config Template”Add this to React app repos as react-doctor.config.json:
{ "lint": true, "deadCode": true, "verbose": true, "diff": false, "failOn": "error", "ignore": { "rules": [], "files": ["src/routeTree.gen.ts", "src/generated/**"], "overrides": [] }}Only ignore rules narrowly. Prefer file-level generated-code ignores over global rule ignores.
Required Commands
Section titled “Required Commands”{ "scripts": { "doctor:react": "react-doctor .", "doctor:react:diff": "react-doctor . --diff main", "doctor:react:staged": "react-doctor . --staged" }}Run before pushing UI work:
pnpm doctor:react:diffMerge rule: React Doctor score should be 75+; scores below 50 block merge unless explicitly approved.
Agent Checklist
Section titled “Agent Checklist”Before handing off React work, an agent should confirm:
- no derived state in effects
- no missing effect cleanup
- no nested component definitions
- no index keys for mutable lists
- no unlabelled icon-only buttons
- no unsafe HTML or client-side secrets
-
pnpm doctor:react:diffpasses or findings are documented