React Best Practices
Most React problems are not framework problems—they are discipline problems. Components grow too large, state lives in the wrong place, effects do work they should never touch, and “optimizations” land before anyone has measured anything. This page distills the practices that consistently produce fast, readable, and maintainable React apps in React 18 and 19, grouped by the area where decisions actually get made.
Build small, pure components
A component should do one thing and read top to bottom in a few seconds. When a component grows past a screenful, splitting it by responsibility almost always improves both clarity and reuse. Keep the render phase pure: read props, state, and context, then return JSX—no mutation, no I/O, no surprises.
// One job: render an avatar. No data fetching, no formatting logic.
function Avatar({ user, size = 40 }) {
return (
<img
src={user.avatarUrl}
alt={user.name}
width={size}
height={size}
style={{ borderRadius: "50%" }}
/>
);
}
Push branching and orchestration up the tree, and keep leaf components dumb. Small, pure components are easier to test, memoize correctly, and reason about under concurrent rendering.
Keep state minimal and colocated
State is the most expensive thing to get wrong. Two rules cover most cases: store the least state you can, and keep it as close as possible to where it is used.
Anything you can compute from existing props or state should be derived during render, not stored in its own state and kept in sync with an effect.
import { useState } from "react";
function Cart({ items }) {
const [coupon, setCoupon] = useState("");
// Derived, not stored — always correct, never stale.
const subtotal = items.reduce((sum, i) => sum + i.price * i.qty, 0);
const discount = coupon === "SAVE10" ? subtotal * 0.1 : 0;
const total = subtotal - discount;
return (
<div>
<p>Total: ${total.toFixed(2)}</p>
<input value={coupon} onChange={(e) => setCoupon(e.target.value)} />
</div>
);
}
Only lift state up when two components genuinely need to share it. Premature global state (a giant context or store) couples unrelated parts of the app and triggers wide re-renders.
If you find yourself writing an effect whose only job is to keep one piece of state in sync with another, you almost certainly want a derived value or a state reducer instead.
Use effects sparingly, and be honest about dependencies
Effects synchronize React with external systems—the DOM, a subscription, a non-React widget, the network. They are not a general-purpose “run some code” hook. Transforming data for rendering and handling user events do not belong in effects.
import { useEffect, useState } from "react";
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const conn = createConnection(roomId);
conn.on("message", (msg) => setMessages((m) => [...m, msg]));
conn.connect();
return () => conn.disconnect(); // cleanup is mandatory, not optional
}, [roomId]); // every reactive value used inside must be listed
return <MessageList items={messages} />;
}
Never lie to the dependency array to silence the linter. A missing dependency means the effect reads stale values; the right fix is to remove the dependency (move it out of the effect) or to restructure the logic—not to delete it from the array.
| Task | Belongs in an effect? |
|---|---|
| Fetching data for the screen | Yes (or use a data library / framework loader) |
| Subscribing to an external store | Yes |
| Transforming props into render output | No — derive during render |
| Responding to a button click | No — use an event handler |
| Resetting state when a prop changes | Usually no — use a key instead |
Measure before you optimize performance
The fastest React code is usually the simplest code. Reach for React.memo, useMemo, and useCallback only after the React DevTools Profiler shows a real, measurable cost. Sprinkling memoization everywhere adds complexity, costs memory, and frequently does nothing because a dependency changes every render anyway.
import { memo, useMemo } from "react";
const Row = memo(function Row({ item }) {
return <li>{item.label}</li>;
});
function Table({ items, query }) {
// Memoize genuinely expensive work, with honest deps.
const filtered = useMemo(
() => items.filter((i) => i.label.includes(query)),
[items, query]
);
return <ul>{filtered.map((item) => <Row key={item.id} item={item} />)}</ul>;
}
For long lists, virtualization beats memoization. For slow transitions, useTransition and useDeferredValue keep the UI responsive without manual caching.
Give lists stable, meaningful keys
Keys let React match list items across renders. Use a stable identifier from your data—never the array index for lists that can reorder, insert, or delete, and never Math.random().
// Good: stable id from the data
{users.map((user) => <UserCard key={user.id} user={user} />)}
Index keys cause React to reuse the wrong DOM nodes, which shows up as inputs holding the wrong values and animations jumping. A stable key is correctness, not just performance.
Build accessible UIs by default
Accessibility is cheapest when it is built in from the start.
- Use semantic elements—
<button>,<nav>,<main>,<label>—instead of<div onClick>. - Associate every input with a
<label>; generate ids withuseIdwhen needed. - Make custom interactive elements keyboard operable and give them correct roles.
- Provide
alttext for images and visible focus states for all controls.
import { useId } from "react";
function Field({ label, value, onChange }) {
const id = useId();
return (
<>
<label htmlFor={id}>{label}</label>
<input id={id} value={value} onChange={onChange} />
</>
);
}
Organize the project by feature
As an app grows, group files by feature, not by file type. Co-locate a feature’s components, hooks, and tests so a change touches one folder, and reserve a shared layer for cross-cutting utilities.
src/
features/
cart/
CartPage.jsx
useCart.js
cart.test.js
auth/
LoginForm.jsx
useAuth.js
shared/
components/
hooks/
lib/
App.jsx
This scales far better than components/, hooks/, and utils/ folders that grow into hundreds of unrelated files.
Best Practices
- Keep components small and pure; push logic up and keep leaves presentational.
- Store the minimum state, colocate it, and derive everything else during render.
- Use effects only to sync with external systems, and never lie to the dependency array.
- Profile before optimizing—measure with React DevTools, then memoize the real hotspots.
- Key lists by a stable id from your data, never by index or random values.
- Bake accessibility in with semantic HTML, labels, and keyboard support from day one.
- Organize source by feature so related code lives together and changes stay local.