Loadable-components: Delay rendering fallback component to avoid "Flash Of Loading Component"

Created on 26 Apr 2019  路  15Comments  路  Source: gregberge/loadable-components

馃殌 Feature Proposal

Avoiding Flash Of Loading Component

Provide an option similar to delay option of react-loadable that sets a number of milliseconds to wait before showing the fallback component. The fallback component, thus will only be rendered if loading takes sufficiently long time. This avoids The Flash of Loading Component.

Additionally, consider to provide a sane default value for this new option. For example, it is 200 ms for react-loadable. For backwards compatibility, consider setting the default to 0 (no delay).

This is similar in nature, but simpler than what is requested in #203 (which itself reminds me of "router events" in Next.js)

Motivation

Flash Of Loading Component, similarly to Flash Of Unstyled Component leads to sub-optimal user experience.

It is best described in the documentation of react-loadable:

Sometimes components load really quickly (<200ms) and the loading screen only quickly flashes on the screen.
A number of user studies have proven that this causes users to perceive things taking longer than they really have. If you don't show anything, users perceive it as being faster.

Source: react-loadable#avoiding-flash-of-loading-component

Additionally to aesthetic side of things, I hypothesize that the unnecessary loading and rendering of the fallback component, in cases where it is only shown for a split second, adds a small performance hit on every new page, without conveying any useful information to the user (it is only visible for a split second).

Example

The interface of the feature could be implemented by providing an additional option for loadable() function:

const OtherComponent = loadable(() => import('./OtherComponent'), {
  delay: 250, // in milliseconds
  fallback: <div>Loading...</div>, // only shown if loading takes more than 250 ms
})

Alternatively, fallback could optionally accept an object with both options:

const OtherComponent = loadable(() => import('./OtherComponent'), { 
  fallback: {
    delay: 250, // in milliseconds
    component: <div>Loading...</div>, // only shown if loading takes more than 250 ms
})

For more advanced use-case, the state of the delay could be exposed to the fallback component so that it would decide for itself what to render (similar to react-loadable):

function Loading(props) {
   if (props.pastDelay) {
    return <div>Loading...</div>;
  } else {
    return null;
  }
}

const OtherComponent = loadable(() => import('./OtherComponent'), { 
  fallback: {
    delay: 250, // in milliseconds
    component: Loading, // if loading takes more than 250 ms, `props.pastDelay` becomes `true`
})

In all cases, when user navigates to the OtherComponent, if OtherComponent manages to load faster than in 250 ms, no fallback (<div>Loading...</div>) is rendered. Otherwise, <div>Loading...</div> starts rendering after 250 ms elapsed and until fallback is ready.

Pitch

Why does this feature belong in the Loadable Component ecosystem:

This feature aligns with the best practices of user experience design and is present in some of the popular competing solutions (e.g.react-loadable)

Most helpful comment

@neoziro It turns out this thing with p-min-delay described in the link above is not what I need. Can we re-open?

The difference:

  • p-min-delay forces fallback to be shown for a given amount of time, regardless of whether the main component loads fast or slow.
  • What I want is the exact opposite: prevent fallback from showing up at all on the fast client (client that loads the main component before a given time threshold).

Is this something that is doable with external tools as well?
Otherwise, can we pass a boolean flag (or even the elapsed time or some sort of a progress metric) to the main loading component similar to how react-loadable does? (3rd code snippet in my first message)
What do you think?

All 15 comments

It looks like this feature can be implemented using external tools:
https://www.smooth-code.com/open-source/loadable-components/docs/delay/
I was not aware about this when created this issue.

I think this solves it for me.

Yes, not built-in. I close it.

@neoziro It turns out this thing with p-min-delay described in the link above is not what I need. Can we re-open?

The difference:

  • p-min-delay forces fallback to be shown for a given amount of time, regardless of whether the main component loads fast or slow.
  • What I want is the exact opposite: prevent fallback from showing up at all on the fast client (client that loads the main component before a given time threshold).

Is this something that is doable with external tools as well?
Otherwise, can we pass a boolean flag (or even the elapsed time or some sort of a progress metric) to the main loading component similar to how react-loadable does? (3rd code snippet in my first message)
What do you think?

In this case, just use a 芦聽fallback聽禄 component that will be displayed only after a short time. You can do it using a 芦聽useState聽禄 + 芦聽useEffect聽禄 that changes the state after a specific amount of time. Do you see what I mean?

Overall - this is not you really need.
What you probably should want - don't display any spinner, ever. There are only two ways to do it:

  • the "old" Suspense (ie "timeout"), which does not exists anymore, but could be emulated in the same way ReactTransitionGroup _keeps_ the old children.
  • delaying routing (or freezing state propagation) until needed chunk is loaded. That might require change _routing library_, as long as no "component based" ones (like react-router) are able to accomplish this, actually quite simple, task.

Should you try to solve it via loadable-components? You might, but it would be not complete.

Or just wait for suspense to be released 馃槄

How do you use pMinDelay and fallback together?

const Loadable = loadable(() =>
  pMinDelay(import('./OtherComponent'), 200)
)

<Loadable fallback="loading..." />

Would be nice if this was built-in.

It's preventing me from switching from react-loadable

Which a lot of people will be in the market for, now that [email protected] displays deprecation warnings

Nowadays expected way to use loadable-components is the use with Suspense, so loading indicator could be not a part of loadable settings, but be configured outside of it.

import {lazy} from '@loadable/component';
const LoadableComponent = lazy(() => import('....');

<Suspense fallback={<ALoadingIndicatorWithADelay />}>
  <LoadableComponent />
</Suspense>

Nowadays expected way to use loadable-components is the use with Suspense, so loading indicator could be not a part of loadable settings, but be configured outside of it.

import {lazy} from '@loadable/component';
const LoadableComponent = lazy(() => import('....');

<Suspense fallback={<ALoadingIndicatorWithADelay />}>
  <LoadableComponent />
</Suspense>

For the <ALoadingIndicatorWithADelay /> component, is it something like:

const LoadingIndicatorWithDelay = () => {
    const delay = 200; // 200ms
    const [showLoadingIndicator, setLoadingIndicatorVisibility] = useState(false);
    setTimeout(() => {
        setLoadingIndicatorVisibility(true);
    }, delay);

    if(showLoadingIndicator) {
        return <LoadingIndicator />;
    }

    return null;
};

For the <ALoadingIndicatorWithADelay /> component, is it something like:

const LoadingIndicatorWithDelay = () => {
  const delay = 200; // 200ms
  const [showLoadingIndicator, setLoadingIndicatorVisibility] = useState(false);
  setTimeout(() => {
      setLoadingIndicatorVisibility(true);
  }, delay);

  if(showLoadingIndicator) {
      return <LoadingIndicator />;
  }

  return null;
};

While this solution works, it will introduce a memory leak and React will show this error:

Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. 
To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.

This is how it should be done following the React docs example: Effects with cleanup

const LoadingIndicatorWithDelay = () => {
  const delay = 200; // 200ms
  const [showLoadingIndicator, setLoadingIndicatorVisibility] = useState(false);

  useEffect(() => {
    const timer = setTimeout(() => setLoadingIndicatorVisibility(true), delay);

    // this will clear Timeout when component unmont like in willComponentUnmount
    return () => {
      clearTimeout(timer);
    };
  });

  return showLoadingIndicator ? <LoadingIndicator /> : null;
};

Nowadays expected way to use loadable-components is the use with Suspense, so loading indicator could be not a part of loadable settings, but be configured outside of it.

import {lazy} from '@loadable/component';
const LoadableComponent = lazy(() => import('....');

<Suspense fallback={<ALoadingIndicatorWithADelay />}>
  <LoadableComponent />
</Suspense>

But Suspense is not SSR

// run only for SSR, where it shall never suspends
React.Suspense = React.Fragment.

@theKashey On latest React versions since 16.11.0 it causes warning. The legacy hydration doesn't expect to meet Suspense Node.

Did not expect server HTML to contain <tag> in <tag>

https://github.com/facebook/react/pull/16945

Was this page helpful?
0 / 5 - 0 ratings

Related issues

zetekla picture zetekla  路  5Comments

mschipperheyn picture mschipperheyn  路  5Comments

gregberge picture gregberge  路  6Comments

lauterry picture lauterry  路  6Comments

zzyxka picture zzyxka  路  7Comments