
React Patterns
- 119 installs
- 14 repo stars
- Updated March 2, 2026
- oakoss/agent-skills
Helps with frontend development tasks during AI-assisted development.
About
react-patterns is a Claude Code skill for frontend development. It helps solo builders move faster with AI-assisted coding.
- react-patterns
- Frontend Development
- AI-coding skill
React Patterns by the numbers
- 119 all-time installs (skills.sh)
- +8 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #1,014 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/oakoss/agent-skills --skill react-patternsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 119 |
|---|---|
| repo stars | ★ 14 |
| Last updated | March 2, 2026 |
| Repository | oakoss/agent-skills ↗ |
What it does
Helps with frontend development tasks during AI-assisted development.
Files
React
Overview
Covers component architecture, performance optimization, state management, data fetching, and modern React 19+ APIs. Prioritizes React Compiler compatibility, Server Components, and elimination of data fetching waterfalls.
When to use: Building React applications, optimizing performance, choosing state management, implementing data fetching, reviewing component architecture.
When NOT to use: Non-React frameworks, purely server-side rendering without React, static sites without interactivity.
Quick Reference
| Pattern | API / Approach | Key Points |
|---|---|---|
| Data fetching | use(dataPromise) | Replaces useEffect+useState fetch pattern |
| Form handling | useActionState(action, init) | Built-in pending states and error handling |
| Optimistic UI | useOptimistic(state, updateFn) | Instant feedback while server processes |
| Non-urgent updates | useTransition() | Mark updates as interruptible |
| Effect events | useEffectEvent(fn) | Reactive values without re-triggering effects |
| Form pending status | useFormStatus() | Read parent form pending state from child component |
| Unique IDs | useId() | Hydration-safe IDs for accessibility |
| Server state | React Query / useSuspenseQuery | Caching, deduplication, background refetch |
| Client state (local) | useState / useRef | Single component or transient values |
| Client state (global) | Zustand / Context | Cross-component client-only state |
| Derived state | Compute during render | Never sync derived values with effects |
| Lazy initialization | useState(() => expensive()) | Avoid eager computation on every render |
| Component types | Page, Feature, UI | Route entry, business logic, reusable primitives |
| Memoization | Trust React Compiler first | Manual useMemo/useCallback only when needed |
| Ref as prop | ref prop on function components | No forwardRef needed in React 19 |
| Ref cleanup | Return function from ref callback | Cleanup runs on detach instead of null call |
| Code splitting | React.lazy() + Suspense | Lazy-load heavy components |
| Parallel fetches | Promise.all() | Eliminate sequential await waterfalls |
| Request dedup | React.cache() | Per-request server-side deduplication |
| Abort server work | cacheSignal() | Cancel expensive async work when client disconnects |
| Resource preloading | prefetchDNS, preconnect, preload, preinit | Optimize resource loading from components |
| State preservation | <Activity> | Hide UI while keeping state mounted |
Common Mistakes
| Mistake | Correct Pattern |
|---|---|
| Fetching data in useEffect with useState | Use the use() API or React Query for data fetching with built-in caching |
| Storing derived values in state and syncing with effects | Compute derived values during render; never use effects for state synchronization |
| Wrapping everything in useMemo and useCallback | Trust React Compiler first; only add manual memoization for expensive computations or memoized children |
Using array.sort() which mutates state | Use array.toSorted() for immutable sorting to avoid unexpected re-renders |
Using && for conditional rendering | Use ternary condition ? <Component /> : null to avoid rendering falsy values like 0 |
| Using Math.random() or Date for IDs | Use useId() for hydration-safe unique identifiers |
| Putting reactive values in effect deps to read latest value | Use useEffectEvent to access latest values without re-triggering effects |
| Creating object literals as effect dependencies | Hoist static objects outside the component or use primitive dependencies |
Using forwardRef in React 19 projects | Pass ref directly as a prop; forwardRef is deprecated in React 19 |
| Mutating props or state during render | Follow Rules of React for React Compiler compatibility: pure renders, no side effects |
Delegation
- Explore component architecture and identify performance bottlenecks: Use
Exploreagent to profile re-renders, analyze bundle size, and trace data fetching waterfalls - Implement React feature with proper patterns: Use
Taskagent to build components following Server Component, Suspense, and React 19 conventions - Design frontend architecture and state management strategy: Use
Planagent to structure component hierarchy, select state management, and plan data fetching approach
If the shadcn-ui skill is available, delegate component variant and theming questions to it.If the tailwind skill is available, delegate utility-first styling and design token questions to it.If the zustand skill is available, delegate global client state management questions to it.References
- Component patterns, state management, and useEffect decisions
- Performance optimization: waterfalls, bundles, and re-renders
- Hooks, Server Actions, and React 19 APIs
- React Compiler patterns and re-render optimization
- Server-side rendering and React Server Components
- Anti-patterns and troubleshooting guide
Anti-Patterns and Troubleshooting
Data Fetching Anti-Patterns
- Fetching in
useEffectwithuseState-- useuse()or React Query - Sequential awaits for independent data -- use
Promise.all() - Fetching the same data in multiple components -- deduplicate with
React.cache()or React Query
State Anti-Patterns
- Storing derived values in state -- compute during render
- Syncing state with effects -- derive inline or use
useSyncExternalStore - Prop drilling through many layers -- use Context or Zustand
- Orphan event listeners -- deduplicate global listeners
- Using index as key for dynamic lists -- use stable unique identifiers
Component Anti-Patterns
- Giant multi-responsibility components -- split into focused pieces
- Untyped props -- always use TypeScript interfaces
- Business logic in UI components -- separate feature from presentation
- Barrel file imports from large libraries -- import specific modules
Performance Anti-Patterns
- Manual
useMemo/useCallbackon everything -- trust React Compiler first array.sort()mutating state -- usetoSorted()for immutability- Eager initialization in
useState-- use lazy initializer function Array.includesin render loops -- useSetwithuseMemofor O(1) lookups- Object literals as props to memoized children -- define outside component or use
useMemo - Effect chains (A triggers B triggers C) -- derive all values directly during render
Troubleshooting
Hydration Mismatch
- Cause: Server and client render different output (dates, random values, browser APIs)
- Fix: Use
useIdfor unique IDs; usesuppressHydrationWarningfor intentional mismatches; defer client-only values to effects
// Bad -- different on server vs client
function Input() {
const id = `input-${Math.random()}`;
return <input id={id} />;
}
// Good -- hydration-safe
import { useId } from 'react';
function Input() {
const id = useId();
return <input id={id} />;
}// Bad -- browser API during SSR
const [width, setWidth] = useState(window.innerWidth);
// Good -- safe initialization
const [width, setWidth] = useState(0);
useEffect(() => {
setWidth(window.innerWidth);
}, []);Unnecessary Re-renders
- Cause: Object/array references changing, subscribing to unused state
- Fix: Defer state reads to callbacks, use primitive dependencies, hoist default non-primitive props, check React Compiler output
Infinite Re-renders
- Cause: Object created during render used as effect dependency, or setState in effect without dependencies
// Bad -- new object every render triggers infinite effect loop
function Component() {
const config = { theme: 'dark' };
useEffect(() => {
applyConfig(config);
}, [config]);
}
// Good -- stable reference outside component
const CONFIG = { theme: 'dark' };
function Component() {
useEffect(() => {
applyConfig(CONFIG);
}, []);
}// Bad -- derived state anti-pattern causes extra renders
function UserList({ users }: { users: User[] }) {
const [filtered, setFiltered] = useState<User[]>([]);
useEffect(() => {
setFiltered(users.filter((u) => u.active));
}, [users]);
}
// Good -- derive during render
function UserList({ users }: { users: User[] }) {
const filtered = useMemo(() => users.filter((u) => u.active), [users]);
}Data Fetching Waterfalls
- Cause: Sequential
awaitcalls, fetch in child after parent renders - Fix: Parallelize with
Promise.all(), hoist fetches, use Suspense boundaries to stream
Bundle Size Bloat
- Cause: Barrel file imports pulling entire libraries, no code splitting
- Fix: Direct path imports, dynamic
import()for heavy components, defer third-party scripts
Stale Closures
- Cause: Event handlers or effects capturing old state values
- Fix: Use functional
setState,useEffectEvent, or refs for latest values
// Bug: count is always 0 inside the interval
useEffect(() => {
const interval = setInterval(() => {
setCount(count + 1); // always sets to 1
}, 1000);
return () => clearInterval(interval);
}, []);
// Fix: functional update
useEffect(() => {
const interval = setInterval(() => {
setCount((prev) => prev + 1);
}, 1000);
return () => clearInterval(interval);
}, []);Race Conditions
- Cause: Old async response overriding newer one when dependencies change quickly
- Fix: AbortController, cancelled flag, or React Query (handles automatically)
// Bug: User A response arrives after User B response
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]);
// Fix: AbortController
useEffect(() => {
const controller = new AbortController();
fetchUser(userId, { signal: controller.signal })
.then(setUser)
.catch((err) => {
if (err.name !== 'AbortError') throw err;
});
return () => controller.abort();
}, [userId]);Memory Leaks
- Cause: Uncleared subscriptions, intervals, or event listeners
- Fix: Always return cleanup functions from effects
useEffect(() => {
const unsubscribe = eventBus.on('event', handler);
return () => unsubscribe();
}, []);Unbounded state growth is another leak pattern:
// Bad -- array grows forever
socket.on('message', (msg) => {
setMessages((prev) => [...prev, msg]);
});
// Good -- limit array size
socket.on('message', (msg) => {
setMessages((prev) => [...prev, msg].slice(-100));
});TypeScript Pitfalls
- Type assertion without validation: Use Zod
parse()instead ofas User - Optional chaining hiding bugs: If
user.profileshould always exist, throw instead of using?. - Non-null assertion (`!`): Handle null case explicitly
- Using `any`: Define proper interfaces for data structures
React Compiler Not Optimizing
- Cause: Side effects during render, mutating props/state, non-idempotent render functions
- Fix: Follow Rules of React -- pure render functions, no side effects, treat props and state as immutable
Component Patterns
Component Architecture
Component types and when to use each:
- Page Components -- Route entry points. Compose feature and UI components.
- Feature Components -- Own business logic, fetch data, manage domain state.
- UI Components -- Reusable primitives with no business logic (buttons, inputs, cards).
export function UserList() {
const { data, isLoading } = useUsers();
if (isLoading) return <LoadingSpinner />;
return (
<div>
{data.map((user) => (
<UserCard key={user.id} user={user} />
))}
</div>
);
}Best practices:
- Type all props with interfaces
- Keep components small and focused
- Compose over inheritance
- Co-locate related code
Component Structure
Follow a consistent internal order:
import { useSuspenseQuery } from '@tanstack/react-query';
import { userQueries } from '@/features/users/api';
import { type User } from '@/features/users/types';
interface UserCardProps {
userId: string;
onSelect?: (user: User) => void;
}
export function UserCard({ userId, onSelect }: UserCardProps) {
const { data: user } = useSuspenseQuery(userQueries.detail(userId));
const fullName = `${user.firstName} ${user.lastName}`;
const handleClick = () => onSelect?.(user);
if (!user.isActive) return null;
return (
<article onClick={handleClick} className="user-card">
<h3>{fullName}</h3>
<p>{user.email}</p>
</article>
);
}Order: imports (external, internal, types) then types then component (queries/mutations, derived state, handlers, early returns, JSX).
State Management
State proximity: keep state as close to where it is used as possible.
| How many components need the state? | Solution |
|---|---|
| One component | useState |
| Parent + children | Props |
| Siblings | Lift to common parent |
| Widely used | Context API |
| Complex app state | Zustand |
| Server data with caching | React Query |
Lazy initialization: pass a function to useState for expensive initial values.
const [data, setData] = useState(() => expensiveComputation());Functional setState: use for stable callbacks that don't need the current value in scope.
setCount((prev) => prev + 1);Refs for transient values: use useRef for values that change frequently but don't need re-renders (scroll position, timers, animation frames).
Derived State
Compute during render, never in effects. Prefer computing values from existing state over synchronizing with effects.
const fullName = `${user.firstName} ${user.lastName}`;
const isEmpty = items.length === 0;
const sortedItems = useMemo(
() => items.toSorted((a, b) => a.name.localeCompare(b.name)),
[items],
);// Bad -- syncing derived state through an effect
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
// Good -- derive inline
const fullName = `${firstName} ${lastName}`;useEffect Decision Tree
Effects are escape hatches. They synchronize React with external systems. If no external system is involved, you likely don't need one.
Do I need useEffect?
Is there an external system involved?
├── No -> Don't use useEffect
│ ├── Derived state? -> Calculate during render
│ ├── Event response? -> Handle in event handler
│ └── Data fetching? -> Use React Query or use()
└── Yes -> Maybe use useEffect
├── Browser APIs (focus, scroll, localStorage)
├── Third-party widgets (maps, charts)
├── Network connections (WebSockets)
└── Analytics/loggingEvent handlers over effects: if logic runs in response to a user action, put it in the event handler, not an effect.
// Bad -- effect chain for event response
useEffect(() => {
if (submitted) {
navigate('/success');
}
}, [submitted]);
// Good -- handle in event
const handleSubmit = async () => {
await submitForm();
navigate('/success');
};Form Handling
Use useActionState for forms with Server Actions (React 19+):
import { useActionState } from 'react';
import { updateProfile } from './actions';
function ProfileForm() {
const [state, action, isPending] = useActionState(updateProfile, {
message: null,
});
return (
<form action={action}>
<input name="username" disabled={isPending} />
<button type="submit" disabled={isPending}>
{isPending ? 'Saving...' : 'Save'}
</button>
{state.message ? <p>{state.message}</p> : null}
</form>
);
}For complex client-side forms with field-level validation, use React Hook Form + Zod or TanStack Form.
Unique IDs with useId
Use useId for hydration-safe unique identifiers. Never use Math.random() or counters for IDs that appear in server-rendered HTML.
import { useId } from 'react';
function EmailField() {
const id = useId();
return (
<div>
<label htmlFor={id}>Email</label>
<input id={id} type="email" />
</div>
);
}Error Handling
- Wrap feature boundaries with Error Boundaries
- Provide fallback UI and retry mechanisms
- Use
getDerivedStateFromErrorfor class-based boundaries
TypeScript
- Type all component props with interfaces
- Type API responses explicitly
- Type state:
useState<User | null>(null) - Use
ReactNodefor children types
Hooks and Server Actions
The use() API (React 19)
Replaces the useEffect + useState fetch pattern. Unwraps promises or context values within the render body. Must be used inside a Suspense boundary when reading promises.
// Bad: The old useEffect boilerplate
function Profile({ id }) {
const [data, setData] = useState(null);
useEffect(() => {
fetchData(id).then(setData);
}, [id]);
if (!data) return <Loading />;
return <div>{data.name}</div>;
}
// Good: Using use() with Suspense
import { use } from 'react';
function Profile({ dataPromise }) {
const data = use(dataPromise);
return <div>{data.name}</div>;
}Sharing Promises Across Components
function Page() {
const dataPromise = fetchData();
return (
<div>
<Suspense fallback={<Skeleton />}>
<DataDisplay dataPromise={dataPromise} />
<DataSummary dataPromise={dataPromise} />
</Suspense>
</div>
);
}
function DataDisplay({ dataPromise }: { dataPromise: Promise<Data> }) {
const data = use(dataPromise);
return <div>{data.content}</div>;
}
function DataSummary({ dataPromise }: { dataPromise: Promise<Data> }) {
const data = use(dataPromise);
return <div>{data.summary}</div>;
}Both components share the same promise, so only one fetch occurs.
Reading Context with use()
use() can also read context, replacing useContext. Unlike useContext, it can be called conditionally:
import { use } from 'react';
function StatusIcon({ isLoggedIn }: { isLoggedIn: boolean }) {
if (isLoggedIn) {
const theme = use(ThemeContext);
return <Icon color={theme.primary} />;
}
return <Icon color="gray" />;
}useActionState (Form Management)
Built-in pending states and error states for forms with Server Actions:
import { useActionState } from 'react';
import { updateProfile } from './actions';
function ProfileForm() {
const [state, action, isPending] = useActionState(updateProfile, {
message: null,
});
return (
<form action={action}>
<input name="username" disabled={isPending} />
<button type="submit" disabled={isPending}>
{isPending ? 'Saving...' : 'Save'}
</button>
{state.message ? <p>{state.message}</p> : null}
</form>
);
}Server Action Pattern
'use server';
export async function updateProfile(
prevState: { message: string | null },
formData: FormData,
) {
const username = formData.get('username');
if (!username) {
return { message: 'Username is required' };
}
await db.user.update({ where: { id: session.userId }, data: { username } });
return { message: 'Profile updated' };
}useOptimistic (Instant UI Feedback)
Show temporary state while an async action is in flight:
import { useOptimistic, useRef, startTransition } from 'react';
function ChatList({
messages,
sendMessage,
}: {
messages: Message[];
sendMessage: (formData: FormData) => Promise<void>;
}) {
const formRef = useRef<HTMLFormElement>(null);
const [optimisticMessages, addOptimisticMessage] = useOptimistic(
messages,
(state, newMessage: string) => [
...state,
{ text: newMessage, sending: true },
],
);
function formAction(formData: FormData) {
const text = formData.get('message') as string;
addOptimisticMessage(text);
formRef.current?.reset();
startTransition(async () => {
await sendMessage(formData);
});
}
return (
<div>
{optimisticMessages.map((m, i) => (
<div key={i}>
{m.text}
{m.sending ? <small> (Sending...)</small> : null}
</div>
))}
<form action={formAction} ref={formRef}>
<input name="message" />
<button type="submit">Send</button>
</form>
</div>
);
}useTransition (Non-Urgent Updates)
Mark non-urgent updates as interruptible for responsive UI:
const [isPending, startTransition] = useTransition();
const handleSearch = (query: string) => {
setQuery(query); // urgent: update input immediately
startTransition(() => {
setFilteredResults(filterItems(query)); // non-urgent: can be interrupted
});
};Calling Server Functions with Transitions
'use client';
import { useState, useTransition } from 'react';
import { updateName } from './actions';
function UpdateName() {
const [name, setName] = useState('');
const [error, setError] = useState<string | null>(null);
const [isPending, startTransition] = useTransition();
const submitAction = () => {
startTransition(async () => {
const result = await updateName(name);
if (result.error) {
setError(result.error);
} else {
setName('');
}
});
};
return (
<form action={submitAction}>
<input type="text" name="name" disabled={isPending} />
{error ? <span>Failed: {error}</span> : null}
</form>
);
}useEffectEvent (React 19.2)
For logic that depends on reactive values but should not trigger the effect. Stable in React 19.2+, replacing the old useRef workaround pattern.
import { useEffect, useEffectEvent } from 'react';
function Chat({ roomId, theme }: { roomId: string; theme: string }) {
const onConnected = useEffectEvent(() => {
logAnalytics('Connected', { theme }); // reads latest theme
});
useEffect(() => {
const socket = connect(roomId);
socket.on('connect', onConnected);
return () => socket.disconnect();
}, [roomId]); // theme is NOT a dependency
}Rules for useEffectEvent:
- Call at the top level of your component
- Only call the returned function inside
useEffect,useLayoutEffect, oruseInsertionEffect - Do not pass effect events to other components or hooks
useId (Hydration-Safe IDs)
Generates unique IDs that are consistent between server and client rendering:
import { useId } from 'react';
function PasswordField() {
const passwordId = useId();
const hintId = useId();
return (
<div>
<label htmlFor={passwordId}>Password</label>
<input id={passwordId} type="password" aria-describedby={hintId} />
<p id={hintId}>Must be at least 8 characters</p>
</div>
);
}Never use Math.random(), incrementing counters, or Date.now() for IDs in server-rendered components.
React Query (Client-Side Server State)
const { data, isLoading, error } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
staleTime: 5 * 60 * 1000,
});Narrow Effect Dependencies
Specify primitive dependencies instead of objects to minimize effect re-runs:
// Better
useEffect(() => {
track(user.id);
}, [user.id]);
// Worse
useEffect(() => {
track(user.id);
}, [user]);Performance Optimization
Eliminating Waterfalls (CRITICAL)
Parallel Execution
// Bad: Sequential -- Total time = sum of all latencies
const user = await fetchUser(); // 200ms
const posts = await fetchPosts(user.id); // 200ms
const config = await fetchConfig(); // 150ms (waits for posts)
// Total: 550ms
// Good: Parallel -- Total time = max(independent) + dependent
const userPromise = fetchUser();
const configPromise = fetchConfig();
const user = await userPromise;
const posts = await fetchPosts(user.id);
const config = await configPromise;
// Total: 400msDefer Await Until Needed
Move await into branches where the value is actually used:
// Bad: blocks both branches
async function handleRequest(userId: string, skipProcessing: boolean) {
const userData = await fetchUserData(userId);
if (skipProcessing) return { skipped: true };
return processUserData(userData);
}
// Good: only blocks when needed
async function handleRequest(userId: string, skipProcessing: boolean) {
if (skipProcessing) return { skipped: true };
const userData = await fetchUserData(userId);
return processUserData(userData);
}Dependency-Based Parallelization
For partial dependencies, use better-all to maximize parallelism:
import { all } from 'better-all';
const { user, config, profile } = await all({
async user() {
return fetchUser();
},
async config() {
return fetchConfig();
},
async profile() {
return fetchProfile((await this.$.user).id);
},
});Suspense Boundaries
Stream content progressively instead of blocking the entire page:
function Page() {
return (
<div>
<div>Sidebar</div>
<div>Header</div>
<Suspense fallback={<Skeleton />}>
<DataDisplay />
</Suspense>
<div>Footer</div>
</div>
);
}
async function DataDisplay() {
const data = await fetchData();
return <div>{data.content}</div>;
}cacheSignal (React 19.2+)
Abort expensive server work when client disconnects:
import { cache, cacheSignal } from 'react';
export const getDeepAnalysis = cache(async (id: string) => {
const signal = cacheSignal();
const response = await fetch(`https://api.internal/analysis/${id}`, {
signal,
next: { revalidate: 3600 },
});
if (signal.aborted) return null;
return response.json();
});Bundle Size (CRITICAL)
Avoid Barrel File Imports
// Bad: imports entire library (200-800ms import cost)
import { Check, X, Menu } from 'lucide-react';
// Good: imports only what you need
import Check from 'lucide-react/dist/esm/icons/check';
import X from 'lucide-react/dist/esm/icons/x';
import Menu from 'lucide-react/dist/esm/icons/menu';Libraries commonly affected: lucide-react, @mui/material, @tabler/icons-react, react-icons, lodash, date-fns.
Dynamic Imports for Heavy Components
import { lazy, Suspense } from 'react';
const MonacoEditor = lazy(() =>
import('./monaco-editor').then((m) => ({ default: m.MonacoEditor })),
);
function CodePanel({ code }: { code: string }) {
return (
<Suspense fallback={<div>Loading editor...</div>}>
<MonacoEditor value={code} />
</Suspense>
);
}Defer Non-Critical Third-Party Libraries
import { useEffect, useState, lazy, Suspense } from 'react';
const Analytics = lazy(() =>
import('@vercel/analytics/react').then((m) => ({ default: m.Analytics })),
);
export default function App({ children }) {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
return (
<div>
{children}
{mounted ? (
<Suspense fallback={null}>
<Analytics />
</Suspense>
) : null}
</div>
);
}Preload on Intent
function EditorButton({ onClick }: { onClick: () => void }) {
const preload = () => {
if (typeof window !== 'undefined') {
void import('./monaco-editor');
}
};
return (
<button onMouseEnter={preload} onFocus={preload} onClick={onClick}>
Open Editor
</button>
);
}Re-render Optimization (MEDIUM)
For React Compiler rules, manual memoization patterns, and detailed re-render diagnosis, see the dedicated React Compiler reference.
useMemo Targeting
// Skip -- cheap operation, memo overhead exceeds recomputation
const doubled = useMemo(() => value * 2, [value]);
// Use -- expensive computation (sorting, filtering large lists)
const sortedItems = useMemo(
() => items.toSorted((a, b) => a.price - b.price),
[items],
);
// Use -- O(1) lookup structure from array
const selectedSet = useMemo(() => new Set(selectedIds), [selectedIds]);useCallback Targeting
// Skip -- no memoized consumer, useCallback adds overhead
const handleClick = useCallback(() => doSomething(), []);
return <button onClick={handleClick}>Click</button>;
// Use -- prevents re-render of memoized child
const handleClick = useCallback(() => doSomething(id), [id]);
return <MemoizedChild onClick={handleClick} />;Stable Object References
// Bad -- object literal creates new reference every render
<Child style={{ color: 'red' }} />;
// Good -- define outside component if static
const style = { color: 'red' };
function Parent() {
return <Child style={style} />;
}Additional Techniques
- Defer state reads -- don't subscribe to state only used in callbacks
- Primitive effect dependencies -- use primitives, not objects
- Hoist default props -- non-primitive defaults outside component
- `startTransition` -- mark non-urgent updates as transitions
- Move effects to events -- interaction logic belongs in event handlers
Rendering (MEDIUM)
- `<Activity>` component (React 19.2+) -- show/hide with preserved state
- CSS `content-visibility` -- skip rendering for off-screen content
- Hoist static JSX -- extract constant JSX outside component functions
- Hydration mismatch prevention -- use
useIdfor stable IDs, inline scripts for client-only data - Ternary conditionals -- prefer
condition ? <A /> : nullovercondition && <A />
JavaScript Micro-Optimizations (LOW-MEDIUM)
- Batch DOM/CSS changes via classes or
cssText - Build
Map/Setfor repeated lookups (O(1) vs O(n)) - Cache property access and function results in loops
- Combine multiple
filter/mapinto single iterations - Use
toSorted()for immutable sorting
React Compiler
How It Works
The React Compiler (stable since React 19) automatically applies memoization equivalent to useMemo, useCallback, and React.memo. Code that follows the Rules of React gets optimized without any manual annotations.
Check if the compiler is active: open React DevTools and look for the "Memo ✨" badge next to optimized components.
Setup
Install as a Babel plugin:
npm i -D babel-plugin-react-compilerFor Next.js, enable in next.config.ts:
const nextConfig: NextConfig = {
experimental: {
reactCompiler: true,
},
};For other Babel-based setups, add to your Babel config:
{
"plugins": ["babel-plugin-react-compiler"]
}The compiler also has experimental support via SWC and Rspack plugins.
Rules for Compiler Compatibility
The compiler requires code to follow three rules during rendering:
1. Pure render functions -- components must return the same JSX for the same props and state. No reading from mutable globals during render. 2. No side effects during render -- no setting timeouts, mutating external variables, or writing to the DOM during the render phase. Event handlers and effects are fine. 3. Immutable props and state -- never mutate objects or arrays that come from props or state. Always create new references.
// Bad -- mutates during render, breaks compiler optimization
function UserList({ users }: { users: User[] }) {
users.sort((a, b) => a.name.localeCompare(b.name));
return (
<ul>
{users.map((u) => (
<li key={u.id}>{u.name}</li>
))}
</ul>
);
}
// Good -- immutable operation, compiler can optimize
function UserList({ users }: { users: User[] }) {
const sorted = users.toSorted((a, b) => a.name.localeCompare(b.name));
return (
<ul>
{sorted.map((u) => (
<li key={u.id}>{u.name}</li>
))}
</ul>
);
}When Manual Memoization Is Still Needed
Even with the compiler, some patterns benefit from explicit memoization:
Expensive Computations
// Compiler handles simple derivations automatically.
// For genuinely expensive work (large list processing, complex math),
// useMemo provides an explicit contract.
const aggregatedData = useMemo(
() => processLargeDataset(rawData, filters),
[rawData, filters],
);Callbacks Passed to Memoized Children
// Skip -- no memoized consumer
const handleClick = () => doSomething();
return <button onClick={handleClick}>Click</button>;
// Use -- prevents re-render of memoized child
const handleClick = useCallback(() => doSomething(id), [id]);
return <MemoizedChild onClick={handleClick} />;Stable Object References for Effect Dependencies
// Bad -- new object every render triggers effect
function Chart({ data }: { data: number[] }) {
const config = { animate: true, duration: 300 };
useEffect(() => {
renderChart(config);
}, [config]);
}
// Good -- stable reference
const CHART_CONFIG = { animate: true, duration: 300 };
function Chart({ data }: { data: number[] }) {
useEffect(() => {
renderChart(CHART_CONFIG);
}, []);
}Re-render Optimization Techniques
Defer State Reads
Do not subscribe to state you only read inside callbacks:
// Bad -- re-renders on every query change
function SearchButton() {
const [query] = useAtom(searchAtom);
const handleClick = () => goToSearch(query);
return <button onClick={handleClick}>Search</button>;
}
// Good -- reads on demand, no subscription
function SearchButton() {
const handleClick = () => {
const query = searchStore.get();
goToSearch(query);
};
return <button onClick={handleClick}>Search</button>;
}Narrow Effect Dependencies
Use primitive values instead of objects to minimize effect re-runs:
// Bad -- re-runs whenever user object reference changes
useEffect(() => {
track(user.id);
}, [user]);
// Good -- only re-runs when the ID actually changes
useEffect(() => {
track(user.id);
}, [user.id]);Hoist Static JSX
Extract constant JSX outside component functions to avoid re-creation:
const EMPTY_STATE = (
<div className="empty">
<p>No items found</p>
</div>
);
function ItemList({ items }: { items: Item[] }) {
if (items.length === 0) return EMPTY_STATE;
return (
<ul>
{items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}Hoist Default Props
Non-primitive default values create new references each render:
// Bad -- new array reference every render
function TagList({ tags = [] }: { tags?: string[] }) {
// ...
}
// Good -- stable default reference
const EMPTY_TAGS: string[] = [];
function TagList({ tags = EMPTY_TAGS }: { tags?: string[] }) {
// ...
}useTransition for Non-Urgent Updates
Mark non-urgent updates as interruptible so the UI stays responsive:
const [isPending, startTransition] = useTransition();
const handleFilter = (value: string) => {
setInputValue(value);
startTransition(() => {
setFilteredResults(filterLargeList(value));
});
};Diagnosing Re-render Issues
1. React DevTools Profiler -- record interactions and check which components re-rendered and why 2. Highlight Updates -- enable "Highlight updates when components render" in React DevTools settings 3. Check Compiler output -- look for the "Memo" badge; missing badge means the component is not being auto-memoized 4. Common culprits:
- Object/array literals as props
- Inline function definitions passed to memoized children
- Subscribing to state not used in the render output
- Effect dependencies using object references instead of primitives
O(1) Lookups with Set
Convert arrays to Sets for repeated membership checks:
// Bad -- O(n) on every render
const isSelected = (id: string) => selectedIds.includes(id);
// Good -- O(1) lookups
const selectedSet = useMemo(() => new Set(selectedIds), [selectedIds]);
const isSelected = (id: string) => selectedSet.has(id);Conditional Rendering
Use ternary operators instead of && to avoid rendering falsy values like 0:
// Bad -- renders "0" when count is 0
{
count && <Badge count={count} />;
}
// Good -- renders null when count is 0
{
count > 0 ? <Badge count={count} /> : null;
}Server-Side Rendering
Partial Pre-rendering (PPR)
Impact: CRITICAL -- Sub-100ms LCP for dynamic pages.
PPR allows a single route to have both static and dynamic parts. The static shell is served immediately from the edge, while dynamic holes (wrapped in Suspense) are streamed in.
// next.config.ts
export default {
experimental: {
ppr: 'incremental',
},
};
// app/dashboard/page.tsx
import { Suspense } from 'react';
import { StaticShell, DynamicStats } from './components';
export const experimental_ppr = true;
export default function Page() {
return (
<main>
<StaticShell />
<Suspense fallback={<StatsSkeleton />}>
<DynamicStats />
</Suspense>
</main>
);
}Rule of Thumb: Anything NOT wrapped in <Suspense> at the page level becomes part of the static pre-rendered shell.
Per-Request Deduplication with React.cache()
Use React.cache() for server-side request deduplication. Authentication and database queries benefit most.
import { cache } from 'react';
export const getCurrentUser = cache(async () => {
const session = await auth();
if (!session?.user?.id) return null;
return await db.user.findUnique({
where: { id: session.user.id },
});
});Cross-Request LRU Caching
React.cache() only works within one request. For data shared across sequential requests, use an LRU cache.
import { LRUCache } from 'lru-cache';
const cache = new LRUCache<string, any>({
max: 1000,
ttl: 5 * 60 * 1000, // 5 minutes
});
export async function getUser(id: string) {
const cached = cache.get(id);
if (cached) return cached;
const user = await db.user.findUnique({ where: { id } });
cache.set(id, user);
return user;
}With Vercel's Fluid Compute, LRU caching is especially effective because multiple concurrent requests share the same function instance.
Minimize Serialization at RSC Boundaries
The React Server/Client boundary serializes all object properties. Only pass fields that the client actually uses.
// Bad: serializes all 50 fields
async function Page() {
const user = await fetchUser();
return <Profile user={user} />;
}
// Good: serializes only 1 field
async function Page() {
const user = await fetchUser();
return <Profile name={user.name} />;
}Parallel Data Fetching with Component Composition
React Server Components execute sequentially within a tree. Restructure with composition to parallelize:
// Bad: Sidebar waits for Page's fetch to complete
export default async function Page() {
const header = await fetchHeader();
return (
<div>
<div>{header}</div>
<Sidebar />
</div>
);
}
// Good: both fetch simultaneously
export default function Page() {
return (
<div>
<Header />
<Sidebar />
</div>
);
}Non-Blocking Post-Response Operations
Schedule work that should execute after a response is sent:
import onFinished from 'on-finished';
app.post('/api/action', async (req, res) => {
await updateDatabase(req.body);
onFinished(res, (err) => {
if (err) return;
logUserAction({
userAgent: req.headers['user-agent'],
status: res.statusCode,
});
});
res.json({ status: 'success' });
});Server Action Security
Authenticate server actions like API routes. Always validate input and check permissions.
Hydration Mismatch Prevention
Use inline <script> to set client-only values before React hydrates. Use suppressHydrationWarning for intentional mismatches.
<Activity> Component (React 19.2+)
Hide a component tree while keeping it mounted and preserving state:
import { Activity } from 'react';
function Dashboard({ activeTab }) {
return (
<>
<Activity mode={activeTab === 'home' ? 'visible' : 'hidden'}>
<HomeTab />
</Activity>
<Activity mode={activeTab === 'settings' ? 'visible' : 'hidden'}>
<SettingsTab />
</Activity>
</>
);
}When mode="hidden", React skips the rendering and commit phase but keeps the DOM nodes and state in memory.