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.xuseRef + transformGood
Sibling list
- Events
- 0
- Renders
- 1
- Frames/s
- –
- Worst frame
- –
events/s0
renders/s0
posRef.current = pfunction 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 msuseRefGood
- Renders/s
- Script time/s
- Frames/s
0.05 ms + 2 msconst 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 transformwrites 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