JSTAcademy
0 XP
Dashboard
Crash Courses
useRef, useMemo & useCallback
10 min
Masters+175 XP
Crash Courses · Masters

useRef, useMemo & useCallback

Access DOM elements and optimize performance with React's memoization hooks.
10 min read+175 XP on completionCert: React Crash Course
Tap any word in the text below to start reading from there.

useRef, useMemo & useCallback

These three hooks give you fine-grained control over DOM access and render performance. They are the tools you reach for when a component is rendering too often or you need direct DOM interaction.

useRef — Two Distinct Use Cases

Use case 1: DOM element access

import { useRef, useEffect } from 'react'

function AutoFocusInput() {
  const inputRef = useRef<HTMLInputElement>(null)

  useEffect(() => {
    inputRef.current?.focus()  // focus the actual DOM input on mount
  }, [])

  return <input ref={inputRef} type="text" placeholder="Auto-focused" />
}

Attaching a ref to a JSX element gives you direct access to the DOM node. React sets inputRef.current to the element after the first render.

Use case 2: Persistent values that do not trigger re-renders

function Timer() {
  const [seconds, setSeconds] = useState(0)
  const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)

  function start() {
    intervalRef.current = setInterval(() => {
      setSeconds(prev => prev + 1)
    }, 1000)
  }

  function stop() {
    if (intervalRef.current) clearInterval(intervalRef.current)
  }

  return (
    <div>
      <p>{seconds}s</p>
      <button onClick={start}>Start</button>
      <button onClick={stop}>Stop</button>
    </div>
  )
}

Storing the interval ID in a ref (not state) means clearing it does not cause a re-render.

useMemo — Expensive Computations

function CourseStats({ courses }: { courses: Course[] }) {
  // Without useMemo: runs on every render, even when courses hasn't changed
  // With useMemo: only recomputes when courses changes
  const stats = useMemo(() => ({
    total: courses.length,
    totalXp: courses.reduce((sum, c) => sum + c.xp, 0),
    byTrack: courses.reduce((acc, c) => {
      acc[c.track] = (acc[c.track] || 0) + 1
      return acc
    }, {} as Record<string, number>),
    masters: courses.filter(c => c.level === 'Masters').length,
  }), [courses])

  return <StatsDisplay stats={stats} />
}

The rule: if a computation takes noticeable time AND the component re-renders often for unrelated reasons, wrap it in useMemo. Do not apply it everywhere — it has overhead of its own.

useCallback — Stable Function References

Every render creates new function instances. If you pass a function as a prop to a memoized child, the child re-renders on every parent render — even if the function "looks the same."

function SearchBar({ onSearch }: { onSearch: (q: string) => void }) {
  const [query, setQuery] = useState('')

  // This function is recreated on every render — SearchButton always re-renders
  const handleSearch = () => onSearch(query)

  // useCallback: same reference until query or onSearch changes
  const handleSearch = useCallback(() => {
    onSearch(query)
  }, [query, onSearch])

  return (
    <div>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      <SearchButton onClick={handleSearch} />
    </div>
  )
}

// memo: only re-renders when props change by reference
const SearchButton = React.memo(({ onClick }: { onClick: () => void }) => (
  <button onClick={onClick}>Search</button>
))

useCallback is most useful when: 1) you pass callbacks to memoized children, or 2) a callback is a dependency of another hook (useEffect, useMemo).

When to Optimize

The golden rule of React performance: measure first, optimize second. Use the React DevTools Profiler to record renders and see which components re-render and why before adding memoization.

Premature optimization with useMemo and useCallback everywhere adds complexity and can actually hurt performance (the comparison overhead on every render). Apply them surgically to confirmed bottlenecks.

0%