diff --git a/src/utils/asyncComponentLoader.js b/src/utils/asyncComponentLoader.js new file mode 100644 index 0000000..78f2a0d --- /dev/null +++ b/src/utils/asyncComponentLoader.js @@ -0,0 +1,115 @@ +import React, { Suspense, useState, useEffect, lazy } from 'react'; + +import Loading from 'components/Loading'; + +import { sleep } from 'utils'; +import { loader } from 'config'; + +// a little bit complex staff is going on here +// let me explain it + +// usually, we load components asynchronously with `Suspense` and `lazy` by this way + +/* + }> + {lazy(_ => import('path/to/the/component'))} + +*/ + +// here we have two major problems: + +// 1) When the loading process is finished "quickly", we will see the fallback component +// has come-and-gone quickly, which will lead to blinking on the page + +// The solution of the first problem is a so-called "delayed fallback", which gives us +// an opportunity to not show the fallback component if the asynchronous process +// is finished until a certain amount of time +// So, the implementation of it is here: + +const getDelayedFallback = (LoadingComponent, delay) => props => { + const [isDelayPassed, setIsDelayPassed] = useState(false); + + useEffect(_ => { + const timerId = setTimeout(_ => setIsDelayPassed(true), delay); + + return _ => clearTimeout(timerId); + }, []); + + return isDelayPassed && ; +} + +/* ================================================================================== */ + +// 2) The second one is the minimum amount of time of fallback render. +// We said that `DelayedFallback` will not show the fallback component in all cases +// when the loading process is finished during the `delay` amount of time, +// but when that process is continuing longer than the `delay`, then the fallback component should +// be appeared. Okay, now let's consider a situation when the loading process finishes a millisecond +// after appearing of the fallback component. We will see the fallback component has come-and-gone +// quickly, which will lead to blinking on the page. + +// The solution of the second problem is the setting up of a minimum timeout, which will +// ensure the minimum amount of time of fallback component render. + +const getLazyComponent = (loadComponent, delay, minimumLoading) => lazy(_ => { + // fix the moment of starting loading + const start = performance.now(); + // start loading + return loadComponent() + .then(moduleExports => { + // loading is finished + const end = performance.now(); + const diff = end - start; + + // first of all, let's remember that there are two values that user provides us + // 1) `loader.delay` - if the loading process is finished during this amount of time + // the user will not see the fallback component at all + // 2) `loader.minimumLoading` - but if it appears, it will stay rendered for at least + // this amount of time + + // so, according to above mentioned, there are three conditions we are interested in + // 1) when `diff` is less than `loader.delay`; in this case, we will immediately return + // the result, thereby we will prevent the rendering of the fallback + // and the main component will be rendered + // 2) when `diff` is bigger than `loader.delay` but less than `loader.delay + loader.minimumLoading`; + // it means `fallback` component has already been rendering and we have to + // wait, starting from this moment, for `loader.delay + loader.minimumLoading - diff` + // amount of time + // 3) when `diff` is bigger than `loader.delay + loader.minimumLoading`. It means we don't need to wait + // anymore and we should immediately return the result as we do it in 1) case. + + // so, in the 1) and 3) cases we return the result immediately, and in 2) case we have to wait + // at least for `loader.delay + loader.minimumLoading - diff` amount of time + + if ( + (diff < loader.delay) || ( + (diff > loader.delay) && (diff > loader.delay + loader.minimumLoading) + ) + ) { + return moduleExports; + } else { + return sleep(loader.delay + loader.minimumLoading - diff).then(_ => moduleExports); + } + }); +}); + +/* ================================================================================== */ + +// And the combination of those two (plus some "magic" plus some backflips), +// will ensure us from having any kind of blinking in the process of asynchronous loadings + +// INFO: the usage of `asyncComponentLoader` looks like this: +// asyncComponentLoader(_ => import('pages/Welcome')) + +const asyncComponentLoader = (loadComponent, loadingProps) => props => { + const Fallback = loader.delay ? getDelayedFallback(Loading, loader.delay) : Loading; + const LazyComponent = getLazyComponent(loadComponent, loader.delay, loader.minimumLoading); + + return ( + }> + + + ); +}; + +export default asyncComponentLoader;