React best practices
Skill Inteligentsensingsolutions/tdd-dev-workflow/skills/react-best-practices
This skill should be used when the user asks about "React performance", "React components", "Server Components", "Client Components", "re-renders", "React hooks", "bundle size", "code splitting", "lazy loading", "error boundaries", "suspense", "React optimization", or needs guidance on React best practices and performance.From its SKILL.md
npx -y skills add Inteligentsensingsolutions/tdd-dev-workflow --skill react-best-practicesAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
10.8 KB, as published. Nobody here has run it
React Best Practices
Performance optimization guidelines adapted from Vercel Engineering's 45 rules across 8 categories.
1. Eliminating Waterfalls
Sequential data fetching creates cascading delays. Eliminate waterfalls at every layer.
Rules
- Fetch independent data in parallel with
Promise.allorPromise.allSettled. - Use Server Components for data fetching to move requests to the server edge.
- Avoid client-side fetch cascades where component A fetches, renders component B, which then fetches.
- Prefetch linked pages with
next/link(automatic in Next.js App Router) or manualrouter.prefetch(). - Colocate data requirements with the component that uses them. Do not hoist fetches to parent components unnecessarily.
- Use
loading.tsxor<Suspense>boundaries to stream partial content while slow requests complete.
Pattern: Parallel Data Fetching
// Server Component
async function Dashboard() {
const [user, stats, notifications] = await Promise.all([
getUser(),
getStats(),
getNotifications(),
]);
return (
<div>
<UserProfile user={user} />
<StatsPanel stats={stats} />
<NotificationList notifications={notifications} />
</div>
);
}
Anti-Pattern: Sequential Fetching
// Each component fetches only after parent renders
function Page() {
const { data: user } = useSWR('/api/user');
// Notifications wait for user to render
return user ? <Notifications userId={user.id} /> : null;
}
2. Bundle Size
Every kilobyte of JavaScript sent to the client costs performance. Minimize aggressively.
Rules
- Use
next/dynamicorReact.lazyfor heavy components not needed on initial render. - Tree-shake imports:
import { specific } from 'lib'instead ofimport lib from 'lib'. - Analyze bundle composition with
@next/bundle-analyzer. - Avoid importing entire libraries when only a single utility is needed.
- Prefer native browser APIs over library equivalents when support is sufficient.
- Mark client-only packages in
next.config.jswithserverExternalPackagesto avoid bundling them server-side.
Pattern: Dynamic Import
import dynamic from 'next/dynamic';
const HeavyChart = dynamic(() => import('@/components/Chart'), {
loading: () => <ChartSkeleton />,
ssr: false,
});
Pattern: Targeted Imports
// Good: tree-shakeable
import { format, parseISO } from 'date-fns';
// Bad: imports entire library
import * as dateFns from 'date-fns';
3. Server-Side Performance
Server Components are the default rendering strategy. Push client boundaries to the leaves.
Rules
- Default to Server Components. Only add
'use client'when the component needs browser APIs, event handlers, or React hooks with state. - Move
'use client'directives to the smallest leaf components possible. - Use streaming with
<Suspense>to progressively render content as data becomes available. - Cache expensive computations and data fetches with
unstable_cacheorReact.cache. - Avoid passing serialization-heavy objects across the server/client boundary.
- Use
generateStaticParamsfor pages that can be statically generated.
Pattern: Leaf Client Component
// layout.tsx - Server Component (no 'use client')
export default function Layout({ children }) {
return (
<main>
<ServerNav /> {/* Server: static nav */}
{children}
<InteractiveFooter /> {/* Client: only this needs interactivity */}
</main>
);
}
4. Client-Side Data Fetching
When client-side data fetching is necessary, use mature libraries with built-in caching.
Rules
- Use TanStack Query or SWR for client-side data fetching. Do not build custom fetch hooks.
- Implement stale-while-revalidate to show cached data immediately while refreshing in the background.
- Deduplicate requests: both TanStack Query and SWR deduplicate identical requests automatically.
- Set appropriate
staleTimeandgcTime(garbage collection time) to avoid redundant network requests. - Prefetch data for routes the user is likely to navigate to.
- Invalidate caches explicitly after mutations.
Pattern: TanStack Query
const { data, isLoading } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
staleTime: 5 * 60 * 1000, // 5 minutes
});
Pattern: Mutation with Invalidation
const mutation = useMutation({
mutationFn: updateUser,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['user'] });
},
});
5. Re-render Optimization
Unnecessary re-renders are the most common source of sluggish React UIs. Measure before optimizing.
Rules
- Use
React.memoonly when React DevTools Profiler shows a component re-rendering with unchanged props. - Lift state up minimally. Only the closest common ancestor that needs the state should hold it.
- Apply
useMemofor expensive computations that recalculate on every render. - Apply
useCallbackto stabilize function references passed as props to memoized children. - Use individual Zustand selectors:
useStore(s => s.count)instead ofuseStore(). - Avoid deriving state in render. Compute derived values in
useMemoor in the store.
Pattern: Selective Zustand Subscription
// Good: only re-renders when count changes
const count = useStore((s) => s.count);
// Bad: re-renders on any store change
const { count } = useStore();
Pattern: Stable Callback
const handleClick = useCallback(
(id: string) => dispatch({ type: 'SELECT', id }),
[dispatch]
);
return <MemoizedList items={items} onSelect={handleClick} />;
6. Rendering Performance
Optimize how React renders large datasets, frequent updates, and complex DOM structures.
Rules
- Virtualize long lists with
react-windowor@tanstack/react-virtual. Never render 1000+ DOM nodes. - Debounce rapid state updates (search input, resize handlers) with
useDeferredValueor a debounce utility. - Avoid inline object/array creation in JSX props. Create them outside the render or memoize them.
- Use CSS for animations instead of React state changes when possible.
- Split large components into smaller pieces with their own
<Suspense>boundaries. - Prefer
keyprop with stable unique IDs, never array indices for dynamic lists.
Pattern: Virtualized List
import { FixedSizeList } from 'react-window';
function VirtualList({ items }: { items: Item[] }) {
return (
<FixedSizeList height={600} itemCount={items.length} itemSize={50} width="100%">
{({ index, style }) => (
<div style={style}>{items[index].name}</div>
)}
</FixedSizeList>
);
}
Anti-Pattern: Inline Objects
// Bad: creates new object every render, breaks memoization
<Component style={{ color: 'red' }} config={{ threshold: 10 }} />
// Good: stable references
const style = useMemo(() => ({ color: 'red' }), []);
const config = useMemo(() => ({ threshold: 10 }), []);
<Component style={style} config={config} />
7. JavaScript Performance
Optimize the JavaScript runtime for computational efficiency.
Rules
- Prefer
MapandSetover plain objects for frequent lookups, additions, and deletions. - Avoid unnecessary array copies. Use mutating methods inside reducers or controlled scopes.
- Use Web Workers for CPU-heavy tasks (image processing, data transformation, encryption).
- Debounce event handlers for scroll, resize, and mousemove events.
- Use
structuredCloneoverJSON.parse(JSON.stringify(...))for deep cloning. - Prefer
for...oforArray.prototypemethods over manual index loops for readability and safety.
Pattern: Web Worker for Heavy Computation
// worker.ts
self.onmessage = (e) => {
const result = heavyComputation(e.data);
self.postMessage(result);
};
// component.tsx
const worker = useMemo(() => new Worker(new URL('./worker.ts', import.meta.url)), []);
8. Anti-Patterns
Recognize and avoid these common mistakes that degrade React application performance.
| Anti-Pattern | Problem | Solution |
|---|---|---|
'use client' everywhere | Ships all components as client JavaScript | Default to Server Components, push 'use client' to leaves |
Fetching in useEffect when server fetch works | Creates waterfalls, shows loading spinners | Fetch in Server Components or use loader |
| Missing loading/error states | Broken UI during network delays or failures | Always provide <Suspense> fallback and error boundaries |
| Prop drilling through many layers | Fragile component coupling, unnecessary re-renders | Use Zustand store or React Context at the right level |
| State for derived values | Redundant state, sync bugs | Compute in useMemo or derive in render |
| Giant monolithic components | Hard to test, slow to re-render | Extract into focused, composable components |
Ignoring key prop warnings | Incorrect reconciliation, lost state | Use stable unique IDs as keys |
| Premature optimization | Code complexity without measured benefit | Profile first with React DevTools, then optimize |
Quick Reference
| Category | Key Rule | Impact |
|---|---|---|
| Waterfalls | Parallel fetches with Promise.all | Reduces load time |
| Bundle Size | Dynamic imports for heavy components | Smaller initial JS |
| Server-Side | Default to Server Components | Less client JS |
| Client Data | TanStack Query / SWR | Cache + dedup |
| Re-renders | Individual Zustand selectors | Fewer re-renders |
| Rendering | Virtualize long lists | Less DOM work |
| JS Perf | Map/Set for lookups | Faster operations |
| Anti-Patterns | Profile before optimizing | Avoid wasted effort |
References
- For the complete 45-rule catalog organized by category, read
references/rules-overview.md. - For detailed individual rules with incorrect/correct code examples, browse
references/rules/. Each file covers one rule with impact rating, tags, and working code. Section prefixes:async-(waterfalls),bundle-(bundle size),server-(SSR),client-(data fetching),rerender-(re-renders),rendering-(DOM),js-(JS perf),advanced-(patterns).
Dependencies
| Skill | Reason |
|---|---|
typescript-pro | React components require strong TypeScript typing for props, generics, and discriminated unions |
Used By
| Skill | Reason |
|---|---|
nextjs-patterns | Next.js builds on React; Server/Client Component rules and rendering strategies apply directly |
zustand-patterns | Zustand store subscriptions and selector patterns depend on React re-render optimization rules |