When does a React ref get attached? And when does a ref callback run?
I ran into a bug with React’s ref, so I decided to write up what I learned.
One-line summary
Render (running the component function body) -> Commit (set ref -> useLayoutEffect) -> Browser paint (this is delegated to the browser) -> useEffect
The reason this order matters is:
- During render, the ref is empty (null)
- The ref is filled in during commit: initialization code that uses the ref must be deferred until after commit
Summary of the React rendering process
- Render Phase (the step that only computes)
- Runs the component function -> only computes the virtual tree. It has nothing to do with the browser DOM.
- At this point there is no real DOM. Therefore
ref.currentis null. - In the render function (the component body), anything that depends on the DOM (measuring styles, focusing, directly registering events, etc.) is forbidden.
- Commit Phase (the step that changes the actual DOM)
- React reflects the result of the virtual tree onto the real screen.
- During commit, React does its work in the following order:
- Apply DOM changes: actually update the DOM, such as creating new nodes / changing attributes / deleting nodes, etc.
- Attach/detach refs
- object ref: filled in via ref.current = domNode
- callback ref: refCallback(domNode) is called (when unmounted, refCallback is called with null for its arguments)
- Run useLayoutEffect (executed synchronously right before paint)
- executed synchronously while the ref is already attached
- therefore it is suitable for
work that must be done before the screen is drawn, such as measuring DOM style, adjusting scroll/focus, or registering synchronous events
- Browser paint: the screen is actually drawn by the browser
- Run useEffect (executed asynchronously): suitable for
work whose timing is less sensitive to the screen, such as network calls, logging, and asynchronous tasks
20250814
Leave a comment