Scope: organization
Status: enabled
Path: frameworks/react.md
This document covers how the organization uses React for frontend applications. It covers component patterns, state management, security, and performance conventions.
- One component per file. File name matches component name exactly (
UserCard.tsx, notuser-card.tsx). - Co-locate tests with components:
UserCard.tsx/UserCard.test.tsx. - Shared components live in
src/components/. Page-specific components live next to their page file.
Do not write new class components. Use function components with hooks.
// ✅ Standard pattern
interface Props {
userId: number;
onUpdate: (user: User) => void;
}
export function UserCard({ userId, onUpdate }: Props) {
// ...
}All components must have explicit TypeScript prop types. Do not use any in component props. Do not use React.FC<Props> — it adds implicit children and obscures the component signature.
- Prefer local state (
useState,useReducer) unless the state is genuinely shared across distant parts of the tree. - Use React Context for low-frequency global state (auth user, theme, locale).
- Use a server-state library (React Query / TanStack Query) for data fetched from an API — do not store server data in Redux or Zustand.
Use TanStack Query as the standard for all server data:
const { data: user, isLoading, error } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
staleTime: 30_000,
});- Define query keys in a central
queryKeys.tsfile to avoid collision. - Set
staleTimeexplicitly — the default of0causes unnecessary refetches. - Handle
errorstates visibly; do not silently swallow fetch errors.
Never use dangerouslySetInnerHTML without sanitizing the content first with DOMPurify. Treat any instance as a code-review blocking issue.
// ❌ PROHIBITED without sanitization
<div dangerouslySetInnerHTML={{ __html: userContent }} />
// ✅ If HTML rendering is genuinely required
import DOMPurify from 'dompurify';
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userContent) }} />REACT_APP_/VITE_prefixed variables are bundled into the client — they are public. Never put API keys, signing secrets, or credentials in them.- Backend API calls must go through the organization's API gateway, not directly to third-party services with credentials embedded in client code.
When building links or image sources from user data, validate the URL scheme:
function SafeLink({ href, children }: { href: string; children: React.ReactNode }) {
const safe = /^https?:\/\//.test(href) ? href : '#';
return <a href={safe} rel="noopener noreferrer">{children}</a>;
}Always add rel="noopener noreferrer" to external links.
Lazy-load routes using React.lazy + Suspense:
const SettingsPage = React.lazy(() => import('./pages/SettingsPage'));
<Suspense fallback={<PageSpinner />}>
<SettingsPage />
</Suspense>useMemoanduseCallbackshould be used where there is a measured performance problem, not preemptively everywhere.React.memois appropriate for components that render frequently with the same props (e.g., list items).
// ❌ Creates a new object on every render, breaking memo
<Component style={{ margin: 8 }} />
// ✅ Define outside the render, or use a CSS class
const STYLE = { margin: 8 } as const;
<Component style={STYLE} />- Every interactive element must be keyboard-operable.
- Images require meaningful
alttext (empty string for decorative images). - Use semantic HTML elements (
<button>,<nav>,<main>) before reaching for<div role="...">. - Run
axe-corein development via the@axe-core/reactpackage — zero violations is the merge bar.