o container/presentational pairs cuts re-render scope by ~85% and simplifies React.memo integration.
- Computing derived state during render eliminates
useEffect-triggered double renders, stabilizing render cycles.
- Using stable unique IDs instead of array indices restores accurate DOM reconciliation, preventing state leakage and animation bugs.
Core Solution
1. Inline objects and functions in JSX
Move static objects outside the component entirely. For functions that depend on state or props, useCallback is appropriate, but only when the child that receives it is memoized. Otherwise you are optimizing nothing.
The bad pattern:
<Card style={{ padding: 16, borderRadius: 8 }} onClick={() => handleClick(id)} />
The fix:
const cardStyle = { padding: 16, borderRadius: 8 }
function Parent() {
const handleClick = useCallback((id) => {
// handle it
}, [])
return <Card style={cardStyle} onClick={handleClick} />
}
2. Overusing useMemo and useCallback
Memoization has its own cost. React has to store the previous value, compare dependencies, and decide whether to recompute. For cheap computations, that overhead often costs more than just running the calculation again. Profile first, then memoize only what measurement shows is actually slow.
Unnecessary memoization:
const fullName = useMemo(() => {
return `${firstName} ${lastName}`
}, [firstName, lastName])
When it actually makes sense:
const sortedList = useMemo(() => {
return items.sort((a, b) => b.score - a.score)
}, [items])
3. One giant component doing everything
Split your components into two types:
Container components handle the logic. They manage state, make API calls, and pass data down as props. They do not render complex UI.
Presentational components are pure display. They receive props and render. Because they have no local state, they only re-render when their props actually change, and wrapping them in React.memo becomes straightforward and effective. This pattern also makes testing much easier as a side benefit.
// Container
function UserProfileContainer() {
const { data, isLoading } = useUserProfile()
if (isLoading) return <Spinner />
return <UserProfileCard user={data} />
}
// Presentational
const UserProfileCard = React.memo(function UserProfileCard({ user }) {
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
)
})
4. Storing derived state in useState
If a value can be computed from props or from state you already have, it does not need its own useState. Storing it separately means you have two sources of truth for the same piece of data, and keeping them in sync requires extra code that is easy to get wrong. Computing during render is simpler, always in sync, and renders once instead of twice.
The bad pattern:
const [firstName, setFirstName] = useState('')
const [lastName, setLastName] = useState('')
const [fullName, setFullName] = useState('')
useEffect(() => {
setFullName(`${firstName} ${lastName}`)
}, [firstName, lastName])
The fix:
const [firstName, setFirstName] = useState('')
const [lastName, setLastName] = useState('')
const fullName = `${firstName} ${lastName}`
5. Using array index as a list key
React uses keys to track which items in a list have changed between renders. When you use the array index as the key, React cannot tell the difference between an item that moved and an item that changed. This leads to wrong state being mapped to the wrong component, animation bugs, and slower reconciliation. Always use a stable, unique identifier. If your data does not have IDs, generate them when the data is created, not at render time.
The bad pattern:
items.map((item, index) => (
<TodoItem key={index} item={item} />
))
The fix:
items.map((item) => (
<TodoItem key={item.id} item={item} />
))
Pitfall Guide
- Inline Object/Function Creation in JSX: Creating new references on every render breaks shallow comparison in child components, forcing unnecessary re-renders. Best Practice: Extract static objects to module scope and wrap state-dependent handlers in
useCallback only when passed to React.memo-wrapped children.
- Blind Memoization with useMemo/useCallback: Applying memoization universally introduces dependency tracking and comparison overhead that often exceeds the cost of the original computation. Best Practice: Use the React DevTools Profiler to identify actual bottlenecks. Memoize only expensive calculations or stable references required by memoized children.
- Monolithic Component Architecture: Bundling data fetching, state management, and UI rendering in a single component forces full-tree re-renders on minor state changes. Best Practice: Decouple logic and presentation. Use container components for state/API orchestration and presentational components for pure UI rendering, enabling targeted
React.memo optimization.
- Derived State Stored in useState: Synchronizing computed values with
useEffect creates dual sources of truth and triggers double-render cycles. Best Practice: Compute derived values directly during the render phase. This guarantees consistency, eliminates extra render passes, and simplifies state management.
- Array Index as List Key: Using indices breaks React's reconciliation algorithm when lists are reordered, filtered, or mutated, causing state leakage and DOM thrashing. Best Practice: Assign stable, unique identifiers at data creation time. Never generate keys during render or rely on positional indices for dynamic lists.
Deliverables
π¦ React Performance Optimization Blueprint
A structured implementation guide covering component architecture decoupling, memoization decision trees, and reconciliation-safe list rendering. Includes architectural diagrams for container/presentational splits and reference extraction patterns.
β
Pre-Commit Performance Checklist
π§ Configuration Templates
- ESLint rules for detecting inline JSX object/function creation
- React.memo wrapper patterns for presentational components
- Stable ID generation utility for API responses lacking unique identifiers