React Best Practices
You are an agent responsible for evaluating a PR for adherence to the React best practices outlined below. Your goal is to identify any poor practices. If you find a poor practice, address it with a code change. Keep all of your changes constrained to a single commit. In the PR description that you create, be sure to reference the relevant best practice when describing a change that you make. If y
Overview
You are an agent responsible for evaluating a PR for adherence to the React best practices outlined below. Your goal is to identify any poor practices. If you find a poor practice, address it with a code change. Keep all of your changes constrained to a single commit. In the PR description that you create, be sure to reference the relevant best practice when describing a change that you make. If you do not find any poor practices, do not open a PR.
IMPORTANT: Check the PR diff and only suggest changes adjacent to/within the scope of the PR. Do not just randomly search through the codebase for potential improvements.
<react-best-practices>React Best Practices
Below is a comprehensive performance optimization guide for React and Next.js applications, prioritized by impact from critical to incremental. Each rule includes detailed explanations, real-world examples comparing incorrect vs. correct implementations, and specific impact metrics to guide automated refactoring and code generation.
Note: If your project has React Compiler enabled, manual memoization (
memo(),useMemo(),useCallback()) and static JSX hoisting are handled automatically.
1. Eliminating Waterfalls
Waterfalls are the #1 performance killer. Each sequential await adds full network latency.
1.1 Defer Await Until Needed (CRITICAL IMPACT)
Move await into branches where data is actually used.
// ❌ Bad: blocks both branches
async function handleRequest(userId: string, skip: boolean) {
const userData = await fetchUserData(userId);
if (skip) return { skipped: true };
return processUserData(userData);
}
// ✅ Good: fetch only when needed
async function handleRequest(userId: string, skip: boolean) {
if (skip) return { skipped: true };
const userData = await fetchUserData(userId);
return processUserData(userData);
}
1.2 Dependency-Based Parallelization (CRITICAL IMPACT)
Use better-all to maximize parallelism when operations have partial dependencies.
// ❌ Bad: profile waits for config unnecessarily
const [user, config] = await Promise.all([fetchUser(), fetchConfig()]);
const profile = await fetchProfile(user.id);
// ✅ Good: config and profile run in parallel
const { user, config, profile } = await all({
async user() {
return fetchUser();
},
async config() {
return fetchConfig();
},
async profile() {
return fetchProfile((await this.$.user).id);
},
});
1.3 Parallelize Independent Operations (CRITICAL IMPACT)
Start independent operations immediately with Promise.all() or by creating promises early.
// ❌ Bad: sequential execution, 3 round trips
const user = await fetchUser();
const posts = await fetchPosts();
const comments = await fetchComments();
// ✅ Good: parallel execution, 1 round trip
const [user, posts, comments] = await Promise.all([
fetchUser(),
fetchPosts(),
fetchComments(),
]);
// ✅ Good: in API routes, start promises early
const sessionPromise = auth();
const configPromise = fetchConfig();
const session = await sessionPromise;
const [config, data] = await Promise.all([
configPromise,
fetchData(session.user.id),
]);
return Response.json({ data, config });
}
1.4 Strategic Suspense Boundaries (CRITICAL IMPACT)
Use Suspense to show wrapper UI immediately while data loads.
// ❌ Bad: entire page blocked by data fetching
async function Page() {
const data = await fetchData();
return (
<div>
</div>
);
}
// ✅ Good: layout renders immediately, data streams in
function Page() {
return (
<div>
}>
</div>
);
}
async function DataDisplay() {
const data = await fetchData();
return <div>{data.content}</div>;
}
2. Bundle Size Optimization
2.1 Avoid Barrel File Imports (CRITICAL IMPACT)
Import directly from source files to avoid loading thousands of unused modules. Barrel files can take 200-800ms just to import.
// ❌ Bad: imports entire library
// ✅ Good: imports only what you need
// ✅ Alternative: Next.js 13.5+ optimizePackageImports
// next.config.js
module.exports = {
experimental: { optimizePackageImports: ["lucide-react", "@mui/material"] },
};
2.2 Conditional Module Loading (HIGH IMPACT)
Load large modules only when features are activated.
function AnimationPlayer({ enabled }: { enabled: boolean }) {
const [frames, setFrames] = useState<Frame[] | null>(null);
useEffect(() => {
if (enabled && !frames && typeof window !== "undefined") {
import("./animation-frames.js").then((mod) => setFrames(mod.frames));
}
}, [enabled, frames]);
if (!frames) return ;
return ;
}
2.3 Defer Non-Critical Libraries (MEDIUM IMPACT)
Load analytics and logging after hydration.
// ❌ Bad: blocks initial bundle
// ✅ Good: loads after hydration
const Analytics = dynamic(
() => import("@vercel/analytics/react").then((m) => m.Analytics),
{ ssr: false },
);
2.4 Dynamic Imports for Heavy Components (CRITICAL IMPACT)
// ❌ Bad: Monaco bundles with main chunk (~300KB)
// ✅ Good: Monaco loads on demand
const MonacoEditor = dynamic(
() => import("./monaco-editor").then((m) => m.MonacoEditor),
{ ssr: false },
);
2.5 Preload on User Intent (MEDIUM IMPACT)
Preload heavy bundles on hover/focus to reduce perceived latency.
function EditorButton({ onClick }: { onClick: () => void }) {
const preload = () => {
void import("./monaco-editor");
};
return (
<button onMouseEnter={preload} onFocus={preload} onClick={onClick}>
Open Editor
</button>
);
}
3. Server-Side Performance
3.1 Cross-Request LRU Caching (HIGH IMPACT)
React.cache() only works within one request. Use LRU for cross-request caching.
const cache = new LRUCache<string, any>({ max: 1000, ttl: 5 * 60 * 1000 });
const cached = cache.get(id);
if (cached) return cached;
const user = await db.user.findUnique({ where: { id } });
cache.set(id, user);
return user;
}
3.2 Minimize Serialization at RSC Boundaries (HIGH IMPACT)
Only pass fields the client actually uses across Server/Client boundaries.
// ❌ Bad: serializes all 50 fields
async function Page() {
const user = await fetchUser();
return ;
}
// ✅ Good: serializes only needed fields
async function Page() {
const user = await fetchUser();
return ;
}
3.3 Parallel Data Fetching with Component Composition (CRITICAL IMPACT)
Restructure components to parallelize data fetching.
// ❌ Bad: Sidebar waits for Page's fetch
const header = await fetchHeader();
return (
<div>
<div>{header}</div>
</div>
);
}
// ✅ Good: both fetch simultaneously
async function Header() {
const data = await fetchHeader();
return <div>{data}</div>;
}
return (
<div>
</div>
);
}
3.4 Per-Request Deduplication with React.cache() (MEDIUM IMPACT)
Use for auth and database queries. Note: Next.js automatically deduplicates fetch calls.
const session = await auth();
if (!session?.user?.id) return null;
return await db.user.findUnique({ where: { id: session.user.id } });
});
3.5 Use after() for Non-Blocking Operations (MEDIUM IMPACT)
Schedule logging/analytics after response is sent.
await updateDatabase(request);
after(async () => {
logUserAction({ userAgent: request.headers.get("user-agent") });
});
return Response.json({ status: "success" });
}
4. Client-Side Data Fetching
4.1 Use Passive Event Listeners (MEDIUM IMPACT)
Add { passive: true } to touch/wheel listeners to enable immediate scrolling.
// ✅ Good: allows browser to scroll immediately
document.addEventListener("touchstart", handler, { passive: true });
document.addEventListener("wheel", handler, { passive: true });
4.2 Use SWR for Automatic Deduplication (MEDIUM IMPACT)
// ❌ Bad: each instance fetches separately
const [users, setUsers] = useState([]);
useEffect(() => {
fetch("/api/users")
.then((r) => r.json())
.then(setUsers);
}, []);
// ✅ Good: multiple instances share one request
const { data: users } = useSWR("/api/users", fetcher);
4.3 Version localStorage Data (MEDIUM IMPACT)
Add version prefix and store minimal fields. Always wrap in try-catch (throws in incognito).
const VERSION = "v2";
function saveConfig(config: { theme: string }) {
try {
localStorage.setItem(`config:${VERSION}`, JSON.stringify(config));
} catch {}
}
5. Re-render Optimization
5.1 Defer State Reads to Usage Point (MEDIUM IMPACT)
Don't subscribe to dynamic state if you only read it in callbacks.
// ❌ Bad: subscribes to all searchParams changes
const searchParams = useSearchParams();
const handleShare = () => {
shareChat(searchParams.get("ref"));
};
// ✅ Good: reads on demand
const handleShare = () => {
const ref = new URLSearchParams(window.location.search).get("ref");
shareChat(ref);
};
5.2 Extract to Memoized Components (MEDIUM IMPACT)
Enable early returns before expensive computation.
// ✅ Good: skips computation when loading
const UserAvatar = memo(function UserAvatar({ user }: { user: User }) {
const id = useMemo(() => computeAvatarId(user), [user]);
return ;
});
function Profile({ user, loading }: Props) {
if (loading) return ;
return ;
}
5.3 Narrow Effect Dependencies (LOW IMPACT)
Use primitives instead of objects; derive booleans from continuous values.
// ❌ Bad: re-runs on any user change
useEffect(() => {
console.log(user.id);
}, [user]);
// ✅ Good: re-runs only when id changes
useEffect(() => {
console.log(user.id);
}, [user.id]);
// ✅ Good: derive boolean from continuous value
const isMobile = width < 768;
useEffect(() => {
if (isMobile) enableMobileMode();
}, [isMobile]);
5.4 Subscribe to Derived State (MEDIUM IMPACT)
// ❌ Bad: re-renders on every pixel
const width = useWindowWidth();
const isMobile = width < 768;
// ✅ Good: re-renders only on boolean change
const isMobile = useMediaQuery("(max-width: 767px)");
5.5 Use Functional setState and Lazy Initialization (MEDIUM IMPACT)
Functional updates prevent stale closures and create stable callbacks. Lazy initialization avoids computation on every render.
// ❌ Bad: stale closure risk, recreates callback
const addItem = useCallback(
(item: Item) => {
setItems([...items, item]);
},
[items],
);
// ✅ Good: stable callback, always uses latest state
const addItem = useCallback((item: Item) => {
setItems((curr) => [...curr, item]);
}, []);
// ❌ Bad: buildIndex runs every render
const [index] = useState(buildSearchIndex(items));
// ✅ Good: buildIndex runs only once
const [index] = useState(() => buildSearchIndex(items));
5.6 Use Transitions for Non-Urgent Updates (MEDIUM IMPACT)
useEffect(() => {
const handler = () => {
startTransition(() => setScrollY(window.scrollY));
};
window.addEventListener("scroll", handler, { passive: true });
return () => window.removeEventListener("scroll", handler);
}, []);
6. Rendering Performance
6.1 CSS content-visibility for Long Lists (MEDIUM IMPACT)
Defer off-screen rendering for 10× faster initial render on long lists.
.message-item {
content-visibility: auto;
contain-intrinsic-size: 0 80px;
}
6.2 Hoist Static JSX Elements (HIGH IMPACT)
Extract static JSX outside components, especially large SVGs.
// ✅ Good: reuses same element
const skeleton = <div className="h-20 animate-pulse bg-gray-200" />;
function Container({ loading }: { loading: boolean }) {
return <div>{loading && skeleton}</div>;
}
6.3 Prevent Hydration Mismatch Without Flickering (LOW IMPACT)
Inject synchronous script to set client-only values before React hydrates.
function ThemeWrapper({ children }: { children: ReactNode }) {
return (
<>
<div id="theme-wrapper">{children}</div>
<script
dangerouslySetInnerHTML={{
__html: `
(function() {
var theme = localStorage.getItem('theme') || 'light';
document.getElementById('theme-wrapper').className = theme;
})();
`,
}}
/>
</>
);
}
6.4 Use Explicit Conditional Rendering (MEDIUM IMPACT)
Use ternary operators to prevent rendering 0 or NaN.
// ❌ Bad: renders "0" when count is 0
{
count && {count};
}
// ✅ Good: renders nothing when count is 0
{
count > 0 ? {count} : null;
}
7. JavaScript Performance
7.1 Batch DOM CSS Changes (MEDIUM IMPACT)
Group CSS changes via classes or cssText to minimize reflows.
// ❌ Bad: multiple reflows
element.style.width = "100px";
element.style.height = "200px";
// ✅ Good: single reflow
element.classList.add("highlighted-box");
7.2 Use Set/Map for O(1) Lookups (LOW IMPACT)
Convert arrays to Set/Map for repeated membership checks.
// ❌ Bad: O(n) per check
items.filter((item) => allowedIds.includes(item.id));
// ✅ Good: O(1) per check
const allowedSet = new Set(allowedIds);
items.filter((item) => allowedSet.has(item.id));
7.3 Build Index Maps for Repeated Lookups (LOW IMPACT)
// ❌ Bad: O(n) per lookup
orders.map((o) => ({ ...o, user: users.find((u) => u.id === o.userId) }));
// ✅ Good: O(1) per lookup
const userById = new Map(users.map((u) => [u.id, u]));
orders.map((o) => ({ ...o, user: userById.get(o.userId) }));
7.4 Cache Expensive Operations (MEDIUM IMPACT)
Cache function results and storag