9

I'm working on an app where we have transitions between pages that we want to delay if the next page has any lazy-loaded components that haven't been loaded yet. So I'm trying to figure out if there's any way to reliably check whether a lazy-loaded component has finished loading yet.

This solution works, but only the first time the lazy-loaded component tries to load -- i.e. not if it renders instantly because the lazy-loaded component is already loaded.

import React, {PropsWithChildren, useEffect} from 'react'

export default function SuspenseTrigger(props) {
  return (
    <React.Suspense fallback={
      <>
        {props.fallback}
        <Trigger onLoad={props.onLoad} onComplete={props.onComplete} />
      </>
    }>
      {props.children}
    </React.Suspense>
  )
}

function Trigger(props) {
  useEffect(() => {
    if (props.onLoad) {
      props.onLoad()
    }

    return () => {
      if (props.onComplete) {
        setTimeout(props.onComplete)
      }
    }
  }, [])

  return <></>
}

This component correctly calls onLoad and onComplete the first time it's loaded. However, on subsequent times, because the lazy-loaded component is now cached, the children are rendered instantly and the fallback is never rendered, which means onLoad and onComplete never get called.

One thing I've tried is putting a second Trigger inside the body of the SuspenseTrigger:

function ensureLoadCompleteCalled() {
  onLoad()
  onComplete()
}

return (
  <React.Suspense fallback={/* same as before */}>
    {props.children}
    <Trigger onLoad={ensureLoadCompleteCalled} />
  </React.Suspense>
)

That doesn't work because the children of Suspense get rendered instantly even when other elements aren't fully loaded. So onLoad and onComplete get called instantly, regardless of whether the Suspense is finished loading or not.

To get around that, I've also tried some fancier state-checking (code on PasteBin). The main tough thing there is checking whether the fallback has been rendered which I can't figure out how to reliably do. I've tried waiting 100 ms before checking but even that doesn't work reliably for some reason. Maybe it's possible with useRef?

Any ideas?

Jan
  • 848
  • 7
  • 17

2 Answers2

0

have you try wrapping your {props.children} on a Loader Functional Component, so on useLayoutEffect yo execute the onComplete function?

Here just a raw idea, but it may work...

const Loader = ({ onLoad, onComplete, children }) => {
  if (onLoad) {
    onLoad();
  }

  useLayoutEffect(() => {
    if (onComplete) {
      onComplete();
    }
  }, []);

  return children;
}
<React.Suspense fallback={/* same as before */}>
    <Loader onLoad={onLoad} onComplete={onComplete}>
      {props.children}
    </Loader>
</React.Suspense>
Luis Sardon
  • 516
  • 3
  • 8
  • I keep getting this Warning: Cannot update a component (`BaseRoutes`) while rendering a different component (`Loader`). To locate the bad setState() call inside `Loader`, follow the stack trace as described in in Loader (at BaseRoutes.js:81) in Suspense (at BaseRoutes.js:80) in Switch (at BaseRoutes.js:74) in BaseRoutes (at Layout.js:55) in Layout (at App.js:21) in Router (created by BrowserRouter) in BrowserRouter (at App.js:20) in App (at src/index.js:55) – Ice_mank Apr 09 '21 at 14:02
0

Would checking _status work?

const Uninitialized = -1;
const Pending = 0;
const Resolved = 1;
const Rejected = 2;

See https://github.com/facebook/react/blob/master/packages/react/src/ReactLazy.js

Josh Unger
  • 6,717
  • 6
  • 33
  • 55