ReactAvoiding Unnecessary Renders

Avoiding Unnecessary Renders

React is fast by default for most apps. But as applications grow, some components re-render far more often than they need to. Each unnecessary render wastes CPU time, can cause UI flicker, and compounds in large trees. The good news: a handful of concrete techniques eliminate the vast majority of unnecessary renders without making code hard to follow.

1. Hoist Stable Objects Outside the Component

Every time a component function runs, any object or array literals inside it are recreated — they are new references even if the values are identical. If those values are passed as props to memoized children or listed in dependency arrays, they cause re-renders or effect re-runs even when nothing logically changed:

JSX
// ✗ New object on every render — defeats any memo on <Chart>
function Dashboard() {
  const chartOptions = { responsive: true, animation: false }  // recreated!
  return <Chart options={chartOptions} />
}

// ✓ Define outside the component — same reference forever
const CHART_OPTIONS = { responsive: true, animation: false }

function Dashboard() {
  return <Chart options={CHART_OPTIONS} />
}
2. useCallback for Event Handlers

Functions defined inside a component are recreated on every render. If you pass them to React.memo-wrapped children, the memo check always fails. useCallback memoizes the function reference:

JSX
import { memo, useState, useCallback } from 'react'

const Button = memo(({ onClick, label }) => {
  console.log('Button rendered:', label)
  return <button onClick={onClick}>{label}</button>
})

function Form() {
  const [name, setName] = useState('')

  // ✗ New function every render — Button always re-renders
  const handleSubmit = () => console.log('Submit:', name)

  // ✓ Same function reference when dependencies haven't changed
  const handleSubmit = useCallback(() => {
    console.log('Submit:', name)
  }, [name])

  return (
    <>
      <input value={name} onChange={e => setName(e.target.value)} />
      <Button onClick={handleSubmit} label="Submit" />
    </>
  )
}
Note
useCallback(fn, deps) is only useful when the function is passed to a memoized child or used in a useEffect/useMemo dependency array. Wrapping every function in useCallback unconditionally adds overhead without benefit.
3. useMemo for Computed Values Passed as Props

When a derived value (sorted list, filtered array, calculated object) is passed to a child, useMemo ensures the reference only changes when the source data changes:

JSX
import { memo, useMemo } from 'react'

const SortedTable = memo(({ rows }) => {
  return <table>{/* render rows */}</table>
})

function ReportPage({ rawData, sortKey }) {
  // ✗ New array on every render — SortedTable always re-renders
  const sorted = [...rawData].sort((a, b) => a[sortKey] > b[sortKey] ? 1 : -1)

  // ✓ New array only when rawData or sortKey changes
  const sorted = useMemo(
    () => [...rawData].sort((a, b) => a[sortKey] > b[sortKey] ? 1 : -1),
    [rawData, sortKey]
  )

  return <SortedTable rows={sorted} />
}
4. React.memo on Leaf / Expensive Components

Wrap components that are expensive to render and receive stable props in React.memo. This is especially effective for repeated items in large lists:

JSX
import { memo } from 'react'

// Without memo: re-renders every time the parent list re-renders,
// even for items whose data didn't change
const ProductCard = memo(function ProductCard({ product, onAddToCart }) {
  return (
    <div>
      <img src={product.imageUrl} alt={product.name} />
      <h3>{product.name}</h3>
      <p>${product.price}</p>
      <button onClick={() => onAddToCart(product.id)}>Add to cart</button>
    </div>
  )
})

// In the parent, stabilize the callback so memo is effective:
const handleAdd = useCallback((id) => {
  setCart(prev => [...prev, id])
}, [])
5. Split Large Components

A component that mixes fast-changing state (like a text input) with slow-changing UI (like a sidebar) causes the slow parts to re-render every keystroke. Extract the fast-changing part into its own component:

JSX
// ✗ Every keystroke re-renders the expensive Sidebar
function Page() {
  const [query, setQuery] = useState('')

  return (
    <div style={{ display: 'flex' }}>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      <ExpensiveSidebar />   {/* re-renders on every keystroke */}
    </div>
  )
}

// ✓ Extract the input — only SearchBar re-renders on keystrokes
function SearchBar() {
  const [query, setQuery] = useState('')
  return <input value={query} onChange={e => setQuery(e.target.value)} />
}

function Page() {
  return (
    <div style={{ display: 'flex' }}>
      <SearchBar />
      <ExpensiveSidebar />   {/* never re-renders due to typing */}
    </div>
  )
}
6. The Children Composition Pattern

When a parent component has its own state, its children are normally re-rendered too. But if children are passed via props.children (or any render prop), React does not re-create them — the parent component created the element, and re-rendering the wrapper doesn't change it:

JSX
// ✗ <ExpensiveTree> re-renders whenever ColorPicker re-renders
function App() {
  const [color, setColor] = useState('blue')

  return (
    <div style={{ color }}>
      <input value={color} onChange={e => setColor(e.target.value)} />
      <ExpensiveTree />   {/* re-renders on every color change */}
    </div>
  )
}

// ✓ Move state into a wrapper; ExpensiveTree is passed as children
function ColoredContainer({ children }) {
  const [color, setColor] = useState('blue')
  return (
    <div style={{ color }}>
      <input value={color} onChange={e => setColor(e.target.value)} />
      {children}   {/* React does not re-render children here */}
    </div>
  )
}

function App() {
  return (
    <ColoredContainer>
      <ExpensiveTree />   {/* only re-renders if App itself re-renders */}
    </ColoredContainer>
  )
}
Tip
The children composition trick is one of the most underused performance techniques. It requires no memoization at all — the key insight is that the parent of <ExpensiveTree> (App) doesn't re-render, so the JSX element React holds for it is the same reference.
7. Granular State Subscriptions

React Context re-renders every consumer whenever any part of the context value changes. For high-frequency updates (store, theme with many values), prefer Zustand or Jotai which support selector-based subscriptions — only the components that care about a specific slice re-render:

JSX
// Context problem: updating cartCount re-renders ALL consumers,
// including components that only use 'user'
const AppContext = createContext()
const { user, cartCount } = useContext(AppContext)  // re-renders on any change

// ──────────────────────────────────────────────────────────────────────
// Zustand solution: component only re-renders when its selected slice changes
import { create } from 'zustand'

const useStore = create(set => ({
  user: null,
  cartCount: 0,
  setUser: user => set({ user }),
  addToCart: () => set(state => ({ cartCount: state.cartCount + 1 })),
}))

// Only re-renders when 'user' changes — cartCount updates do NOT trigger this
function UserGreeting() {
  const user = useStore(state => state.user)
  return <span>Hello, {user?.name}</span>
}

// Only re-renders when 'cartCount' changes — user updates do NOT trigger this
function CartBadge() {
  const cartCount = useStore(state => state.cartCount)
  return <span>{cartCount}</span>
}
8. key to Control Component Identity

React uses key to track component identity across renders. Setting a different key forces React to unmount the old instance and mount a fresh one — which resets all state. This can replace complex reset logic and also prevent stale state bugs:

JSX
// ✗ When userId changes, UserProfile keeps its old state (stale cache)
function App({ userId }) {
  return <UserProfile userId={userId} />
}

// ✓ Different key → React unmounts old instance, mounts fresh one
// All useState/useEffect in UserProfile start clean
function App({ userId }) {
  return <UserProfile key={userId} userId={userId} />
}

// Also useful for forcing a form to reset when a dialog closes:
{isOpen && <EditModal key={editTarget.id} target={editTarget} />}
Warning
Using key to reset components causes a full unmount/remount — useful for resetting state, but be aware that all child components re-mount too. Don't use this as a general performance tool; it is specifically for controlled component identity resets.
  • Hoist static objects and arrays outside the component — same reference, zero cost.

  • useCallback stabilizes function references for memoized children.

  • useMemo stabilizes computed values passed as props.

  • React.memo skips re-renders when props are shallowly equal.

  • Split components to contain fast-changing state near where it is used.

  • The children pattern lets state changes in a wrapper skip expensive subtrees.

  • Zustand/Jotai selectors give component-level granularity without Context re-render storms.

  • key controls component identity — use it to explicitly reset state.