Wait for useCallback effects on DOM

Viewed 544

I'm calling a useCallback from a useEffect that will make the component's height change. Then, to end the effect, I'd like to scroll the component to a specific point. The issue is that the scroll operation does not work because it fires to soon : the DOM did not update yet.

If I wrap it in a setTimeout it does work (because it fires once the DOM updated), but it's kind of hacky.

Is there a clean way to wait for the DOM to render before pursuing the effect?

Does not work:

const viewportElement = useRef()
cont [innerHeight, setInnerHeight] = useState(0)

refreshDOM = useCallback(() => {
  setInnerHeight(500) // Will cause the DOM to refresh and the inner div to grow in height.
}, [setInnerHeight])

useLayoutEffect(() => {
  refreshDOM() // DOM updates
  viewportElement.current.scroll(0, 200) // Fires to soon: innerDiv is still too small 
}, [refreshDOM, someOtherStuffTriggeringTheEffect])

return (
  <div ref={viewportElement} style={{height: 100}}>
    <div style={{height: innerHeight}}></div>
  </div>
)

Works:


// ...

useLayoutEffect(() => {
  refreshDOM() // DOM updates
  setTimeout(
    () => viewportElement.current && viewportElement.current.scroll(0, 200),
    0,
  ) // DOM was refreshed, scroll operation works
}, [refreshDOM, someOtherStuffTriggeringTheEffect])

// ...

2 Answers

A possible solution is add the another useEffect and pass to dependencies innerHeight. When innerHeight is updated it means that you can invoke scrolling:

useEffect(() => {
  viewportElement.current.scroll(0, 200)
}, [innerHeight]);

And don't forget to remove scrolling from useLayoutEffect

Just an addition to Rukkiesman answer

The reason it's not working is cause, setting the height is async. Which means the height hasn't been updated at the time you called viewportElement.current.scroll(0, 200)

The reason wrapping it in a setTimeout works, is cause you've add it to the micro task queue, which would resolve after react is done with the update work.

Related