Keep fast values in refs

Values that change every frame but never render belong in a ref.

From vercel-react-best-practices, rule rerender-use-ref-transient-values.

Cursor follower

Move the pointer across each pad, or press Sweep.

useStateBad
Sibling list
    Events
    0
    Renders
    1
    Frames/s
    –
    Worst frame
    –
    events/s0
    renders/s0
    setPos(p) → left: pos.x
    useRef + transformGood
    Sibling list
      Events
      0
      Renders
      1
      Frames/s
      –
      Worst frame
      –
      events/s0
      renders/s0
      posRef.current = p

      function Follower() {
        const posRef = useRef({ x: 0, y: 0 })
        const dotRef = useRef(null)
      
        const onPointerMove = (e) => {
          const r = e.currentTarget.getBoundingClientRect()
          posRef.current = { x: e.clientX - r.left, y: e.clientY - r.top }
          dotRef.current.style.transform =
            `translate(${posRef.current.x}px, ${posRef.current.y}px)`
        }
      
        return (
          <div className="pad" onPointerMove={onPointerMove}>
            <div ref={dotRef} className="dot" />
            <HeavyList />
          </div>
        )
      }

      Renders vs the frame budget

      Pick a refresh rate, then drag Render cost.

      useStateBad

      Renders/s
      Script time/s
      Frames/s
      render + 2 ms
      useRefGood

      Renders/s
      Script time/s
      Frames/s
      0.05 ms + 2 ms

      const budget = 1000 / hz // 16.7 ms at 60 Hz, 8.3 at 120, 6.9 at 144
      
      // useState: one render per pointermove, one pointermove per frame
      const framesPerUpdate = Math.ceil((renderMs + browserMs) / budget)
      const fps = hz / framesPerUpdate
      
      // useRef: one style write (~0.05 ms), 0 renders

      Drag in a ref, commit once

      Drag each card, or focus it and hold an arrow key.

      useStateBad
      Renders
      1
      Last drag
      0
      Saved at
      –
      onPointerMove: setPos(p)
      useRef, state on dropGood
      Renders
      1
      Last drag
      0
      Saved at
      –
      onPointerUp: setSaved(live.current)

      function DraggableCard() {
        const [saved, setSaved] = useState({ x: 0, y: 0 })
        const live = useRef(saved)
        const grab = useRef(null)
        const cardRef = useRef(null)
      
        const onPointerDown = (e) => {
          e.currentTarget.setPointerCapture(e.pointerId)
          grab.current = { dx: e.clientX - live.current.x, dy: e.clientY - live.current.y }
        }
        const onPointerMove = (e) => {
          if (!grab.current) return
          live.current = { x: e.clientX - grab.current.dx, y: e.clientY - grab.current.dy }
          cardRef.current.style.transform = `translate(${live.current.x}px, ${live.current.y}px)`
        }
        const onPointerUp = () => {
          grab.current = null
          setSaved(live.current)
        }
      
        return (
          <>
            <div ref={cardRef} className="card" onPointerDown={onPointerDown}
              onPointerMove={onPointerMove} onPointerUp={onPointerUp} />
            <p>Saved at {saved.x}, {saved.y}</p>
          </>
        )
      }

      Why it matters, pros and cons

      Why it matters

      A setState per pointermove renders every frame. A ref write renders nothing.

      Pros

      • One render instead of one per frame
      • Siblings stay untouched without memo
      • transform writes skip layout

      Cons

      • React can't see ref changes; rendered text goes stale
      • Setting the same property in JSX overwrites your write
      • Low-frequency values like a toggle belong in state