VikR
VikR

Reputation: 5142

React Hooks: Lazy Loading Breaks useLayoutEffect?

My web app has a top navbar that can change in height. It's pinned to the top of the screen, with css position: fixed. To move the page content below it, I have a spacer div, that updates its height to match the height of the header, and pushes everything else down.

I'm updating my app to use React hooks. I have a useLayoutEffect hook that checks the height of the navbar and updates the height of the spacer to match. Per the React docs:

Unlike componentDidMount or componentDidUpdate, effects scheduled with useEffect don’t block the browser from updating the screen. This makes your app feel more responsive. The majority of effects don’t need to happen synchronously. In the uncommon cases where they do (such as measuring the layout), there is a separate useLayoutEffect Hook with an API identical to useEffect.

What I seem to be finding, is that React lazy loading breaks useLayoutEffect. Here's a CodeSandBox:

https://codesandbox.io/s/5kqxynp564

There is a div hidden by the navbar, containing the text "IF YOU CAN SEE THIS USELAYOUTEFFECT IS WORKING". When direct import is used, the div above that text is sized to match the navbar, pushing the text down below the navbar, and the text becomes visible. When React.lazy is used, useLayoutEffect (in AppBarHeader.js) fails to work.

The lazy loading happens in App.js:

//LOADING <COUNTER/> THIS WAY WORKS
// import Counter from "./Counter";

//LAZY LOADING <COUNTER/> THIS WAY BREAKS useLayoutEffect
const CounterPromise = import("./Counter");
const Counter = React.lazy(() => CounterPromise);

What am I misssing?

Upvotes: 2

Views: 2227

Answers (1)

Ryan Cogswell
Ryan Cogswell

Reputation: 81056

It does appear that this is an actual bug in the timing of the execution of useLayoutEffect for a component within Suspense where some other component is lazy loaded. If AppBarHeader is lazy loaded and Counter is not, it works fine, but with Counter lazy loaded, AppBarHeader's useLayoutEffect executes before it is fully rendered in the DOM (height is still zero).

This sandbox "fixes" it in a very hackish way that helps further demonstrate what is going on.

I added the following to App.js:

  replaceRefHeader = () => {
    this.setState({ ref_header: React.createRef() });
  };

and then passed this to Counter:

<Counter replaceRefHeader={this.replaceRefHeader} history={routeProps.history} />

Then in Counter:

const Counter = ({ replaceRefHeader }) => {
  useLayoutEffect(() => {
    replaceRefHeader();
  }, []);

and then I put [props.ref_header] in the dependency array of AppBarHeader's useLayoutEffect.

I'm not proposing that this is what you should do (though something like this could work as a temporary workaround). This was just to demonstrate the timing issue more clearly.

I've logged a new issue here: https://github.com/facebook/react/issues/14869

Upvotes: 2

Related Questions