Reputation: 1512
Say, I want to measure a DOM element as described in React’s hooks FAQ with an additional twist: the result needs to be applied to another element before the browser repaints. As far as I understand, setState
does not guarantee that the update will be applied immediately unless it’s inside useLayoutEffect
. However, effects should not be used for this purpose either. What would be the correct approach then?
To sum up, I’d like to achieve the same thing as in the example below, but without 0px
briefly flashing before setHeight
has any effect.
function MeasureExample() {
const [height, setHeight] = useState(0);
const measuredRef = useCallback(node => {
if (node !== null) {
setHeight(node.getBoundingClientRect().height);
}
}, []);
return (
<>
<h1 ref={measuredRef}>Hello, world</h1>
<h2>The above header is {Math.round(height)}px tall</h2>
</>
);
}
Upvotes: 1
Views: 823
Reputation: 7073
I had a similar concern however from my tests the snippet you've taken from the React docs does run before a paint occurs. i.e. it doesn't flash 0px
before rendering the measured height.
Practically speaking, you can prove this using the chrome profiler which will spit out screenshots. Alternatively, you can slow down the callback and see it never paints 0px
.
NOTE: Debug statements aren't a good way to test this because they will pause JS execution, but allow paints (at least in Chrome).
function MeasureExample() {
const [height, setHeight] = useState(0);
const measuredRef = useCallback(node => {
if (node !== null) {
for (let i = 0; i < 500000000; i++) {}
setHeight(node.getBoundingClientRect().height);
}
}, []);
return (
<>
<h1 ref={measuredRef}>Hello, world</h1>
<h2>The above header is {Math.round(height)}px tall</h2>
</>
);
}
Theoretically speaking, it has to execute the ref callback before paint because DOM refs are available in useLayoutEffect
which we know runs before paint.
function MeasureExample2() {
const [height, setHeight] = useState(0);
const nodeRef = useRef();
useLayoutEffect(() => {
if (!nodeRef.current) {
throw new Error('Expected el'); // Never called
}
setHeight(node.getBoundingClientRect().height);
});
return (
<>
<h1 ref={nodeRef}>Hello, world</h1>
<h2>The above header is {Math.round(height)}px tall</h2>
</>
);
}
Additionally, setState
within useLayoutEffect
will run synchronously; if it didn't it would make useLayoutEffect
rather painful as you'd have to imperatively mutate the DOM rather than using React render.
I think therefore React must:
Where steps 1, 2 and 3 are all synchronous meaning the browser hasn't had the opportunity to paint yet.
Upvotes: 1
Reputation: 23084
Only render the h2 when you have a non-zero height.
{height && (<h2>The above header is {Math.round(height)}px tall</h2>)}
you could also use css property visibility: hidden on the entire component until it has been measured.
<div style={{visibility: height ? 'visible' : 'hidden'}}>
<h1 ref={measuredRef}>Hello, world</h1>
<h2>The above header is {Math.round(height)}px tall</h2>
</div>
Upvotes: 0