Lesson 11 of 12
Performance: memo, useMemo & useCallback
You will learn how to use memo, useMemo and useCallback to avoid unnecessary re-renders. React is fast by default, so use these only where you have measured a problem.
import { memo, useMemo, useCallback, useState } from 'react'
// memo skips re-rendering a component when its props are shallowly equal.
const Row = memo(function Row({ item, onSelect }) {
console.log('rendering', item.id)
return <li onClick={() => onSelect(item.id)}>{item.name}</li>
})
function List({ items }) {
const [query, setQuery] = useState('')
const [selected, setSelected] = useState(null)
// useMemo: skip an expensive calculation when its inputs have not changed.
const filtered = useMemo(
() => items.filter(i => i.name.toLowerCase().includes(query.toLowerCase())),
[items, query]
)
// useCallback: keep the SAME function identity between renders, so the
// memoized Row above actually stays memoized.
const handleSelect = useCallback(id => setSelected(id), [])
return (
<>
<input value={query} onChange={e => setQuery(e.target.value)} />
<ul>
{filtered.map(item => (
<Row key={item.id} item={item} onSelect={handleSelect} />
))}
</ul>
</>
)
}
// Without useCallback, handleSelect is a NEW function every render, so every
// Row gets a new prop, so memo never skips anything. memo and useCallback
// only work as a pair.import { useState, lazy, Suspense } from 'react'
// The cheapest optimizations are structural — no hooks required.
// 1. Move state DOWN so fewer components re-render.
function SlowPage() {
const [text, setText] = useState('') // every keystroke re-renders ExpensiveTree
return (
<>
<input value={text} onChange={e => setText(e.target.value)} />
<ExpensiveTree />
</>
)
}
function FastPage() {
return (
<>
<TextField /> {/* state lives here now */}
<ExpensiveTree /> {/* untouched by typing */}
</>
)
}
// 2. Pass expensive subtrees as children so they keep the same element identity.
function Wrapper({ children }) {
const [count, setCount] = useState(0)
return (
<div onClick={() => setCount(count + 1)}>
{count}
{children} {/* does not re-render when count changes */}
</div>
)
}
// 3. Lazy-load routes and heavy components
const Chart = lazy(() => import('./Chart'))
function Report() {
return (
<Suspense fallback={<Spinner />}>
<Chart />
</Suspense>
)
}import { useState, useDeferredValue, useTransition, memo } from 'react'
// useDeferredValue: keep the input responsive while a heavy list catches up.
function SearchPage({ allItems }) {
const [query, setQuery] = useState('')
const deferredQuery = useDeferredValue(query)
const isStale = query !== deferredQuery
return (
<>
<input value={query} onChange={e => setQuery(e.target.value)} />
<div style={{ opacity: isStale ? 0.6 : 1 }}>
<ExpensiveResults query={deferredQuery} items={allItems} />
</div>
</>
)
}
const ExpensiveResults = memo(function ExpensiveResults({ query, items }) {
const results = items.filter(i => i.name.includes(query))
return <ul>{results.map(r => <li key={r.id}>{r.name}</li>)}</ul>
})
// useTransition: mark an update as low priority so urgent ones interrupt it.
function TabBar() {
const [tab, setTab] = useState('home')
const [isPending, startTransition] = useTransition()
function select(next) {
startTransition(() => setTab(next)) // the click feedback stays instant
}
return (
<>
<button onClick={() => select('reports')}>Reports</button>
{isPending && <Spinner />}
<TabPanel tab={tab} />
</>
)
}Finished reading?Mark it done to track your progress.
Tip: use the ← and → keys to move between lessons.