React patterns — always apply error boundaries, loading states, accessible markup, proper hooks, controlled forms, stable keys, and correct memoization
87
80%
Does it follow best practices?
Impact
99%
1.83xAverage score across 5 eval scenarios
Passed
No known issues
These patterns must be applied proactively in every React component you write, even when the user does not ask for them. They are not optional extras — they are the baseline for production-quality React code.
Every page or route-level component MUST be wrapped in an error boundary. Do not wait to be asked. If a component renders data, it needs an error boundary above it.
Error boundaries must be class components:
class ErrorBoundary extends React.Component<
{ children: React.ReactNode; fallback?: React.ReactNode },
{ error: Error | null }
> {
state = { error: null as Error | null };
static getDerivedStateFromError(error: Error) { return { error }; }
componentDidCatch(error: Error, info: React.ErrorInfo) {
console.error('Component error:', error, info.componentStack);
}
render() {
if (this.state.error) {
return this.props.fallback || (
<div role="alert">
<h2>Something went wrong</h2>
<button onClick={() => this.setState({ error: null })}>Try again</button>
</div>
);
}
return this.props.children;
}
}
// Always wrap at route/page level
<ErrorBoundary fallback={<p>Dashboard unavailable</p>}>
<Dashboard />
</ErrorBoundary>The fallback UI MUST use role="alert" so screen readers announce the error.
Never render only the happy path. Every component that fetches data MUST handle:
function MenuPage() {
const { menu, loading, error } = useMenu();
if (loading) return <p>Loading menu...</p>;
if (error) return <div role="alert"><p>{error}</p><button onClick={retry}>Retry</button></div>;
if (menu.length === 0) return <p>No menu items available.</p>;
return <MenuList items={menu} />;
}Always clear error state on successful fetch (setError(null)).
These are mandatory on every component, not just when accessibility is mentioned.
Use role="alert" on ANY element whose content appears dynamically to communicate an error or warning:
// ALWAYS do this
{error && (
<div role="alert">
<p>{error}</p>
<button onClick={onRetry}>Retry</button>
</div>
)}Every required form input MUST have all three attributes:
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
aria-required="true"
aria-invalid={!!errors.email}
aria-describedby={errors.email ? 'email-error' : undefined}
/>
{errors.email && <span id="email-error" role="alert">{errors.email}</span>}Use aria-live="polite" on containers whose content updates without a page navigation:
<div aria-live="polite">
{loading ? <p>Loading...</p> : <p>{results.length} results found</p>}
</div>Every form MUST use:
value + onChange)e.preventDefault() on submitnoValidate on the <form> elementfunction OrderForm({ onSubmit }: OrderFormProps) {
const [name, setName] = useState('');
const [error, setError] = useState('');
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!name.trim()) {
setError('Name is required');
return;
}
setError('');
onSubmit({ customerName: name.trim() });
}
return (
<form onSubmit={handleSubmit} noValidate>
<label htmlFor="customer-name">Your name</label>
<input
id="customer-name"
value={name}
onChange={(e) => setName(e.target.value)}
aria-required="true"
aria-invalid={!!error}
aria-describedby={error ? 'name-error' : undefined}
/>
{error && <span id="name-error" role="alert">{error}</span>}
<button type="submit">Place Order</button>
</form>
);
}Always use a stable unique identifier from the data as the key prop. Never use the array index:
// WRONG — index as key breaks when items reorder, insert, or delete
{items.map((item, index) => (
<ItemCard key={index} item={item} />
))}
// ALWAYS do this — stable unique identifier from the data
{items.map(item => (
<ItemCard key={item.id} item={item} />
))}React.memo, useCallback, and useMemo only work when used together. Apply the full pattern when a parent re-renders frequently and passes props to child components:
// Parent component
function Dashboard({ widgets }: { widgets: Widget[] }) {
const sortedWidgets = useMemo(
() => [...widgets].sort((a, b) => a.priority - b.priority),
[widgets]
);
const handleRefresh = useCallback((id: string) => {
setRefreshing(prev => new Set(prev).add(id));
}, []);
return sortedWidgets.map(w => (
<MemoizedWidget key={w.id} widget={w} onRefresh={handleRefresh} />
));
}
// Child MUST be wrapped in React.memo for useCallback to matter
const MemoizedWidget = React.memo(function Widget({ widget, onRefresh }: WidgetProps) {
return (
<div>
<h3>{widget.title}</h3>
<button onClick={() => onRefresh(widget.id)}>Refresh</button>
</div>
);
});// WRONG — useCallback with no memoized child
const handleClick = useCallback(() => { doSomething(); }, []);
return <button onClick={handleClick}>Click</button>;
// WRONG — React.memo defeated by inline object prop
<MemoizedWidget config={{ theme: 'dark' }} />
// RIGHT — stabilize object props with useMemo
const config = useMemo(() => ({ theme: 'dark' }), []);
<MemoizedWidget config={config} />// WRONG — new object every render causes infinite loop
const options = { serverUrl: 'https://localhost:1234', roomId };
useEffect(() => { ... }, [options]);
// RIGHT — move object creation inside the effect, depend on primitives
useEffect(() => {
const options = { serverUrl: 'https://localhost:1234', roomId };
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [roomId]);// WRONG — extra render cycle
const [total, setTotal] = useState(0);
useEffect(() => {
setTotal(items.reduce((sum, i) => sum + i.price, 0));
}, [items]);
// RIGHT — compute during render
const total = useMemo(
() => items.reduce((sum, i) => sum + i.price, 0),
[items]
);// WRONG — stale closure
setCount(count + 1);
// RIGHT
setCount(c => c + 1);useEffect(() => {
let cancelled = false;
async function load() {
const res = await fetch('/api/data');
const data = await res.json();
if (!cancelled) setData(data);
}
load();
return () => { cancelled = true; };
}, []);Always apply:
any)role="alert" on all dynamically-appearing error messagesaria-required="true" on all required form inputsaria-invalid bound to error state on form inputsaria-describedby linking inputs to their error message elementsaria-live="polite" on dynamically updating content regionsnoValidate and e.preventDefault()useCallback only for functions passed to React.memo childrenReact.memo children don't receive inline objects/arrays/functionsuseMemo for derived data passed to memoized childrenany