diff --git a/src/components/AsyncComponentLoader/Component.js b/src/components/AsyncComponentLoader/Component.js deleted file mode 100644 index 69018ef..0000000 --- a/src/components/AsyncComponentLoader/Component.js +++ /dev/null @@ -1,13 +0,0 @@ -import React, { Suspense } from 'react'; - -import Loading from 'components/Loading'; - -const AsyncComponentLoader = (Component, loadingProps) => props => { - return ( - }> - - - ); -}; - -export default AsyncComponentLoader; diff --git a/src/components/AsyncComponentLoader/index.js b/src/components/AsyncComponentLoader/index.js deleted file mode 100644 index bf109b8..0000000 --- a/src/components/AsyncComponentLoader/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import Component from './Component'; - -export default Component; diff --git a/src/components/Page/styles.js b/src/components/Page/styles.js index 65935c4..7af1bb8 100644 --- a/src/components/Page/styles.js +++ b/src/components/Page/styles.js @@ -6,6 +6,7 @@ const useStyles = makeStyles(theme => ({ root: { height: '100%', overflow: 'hidden', + position: 'relative', 'padding-left': theme.spacing(isMobile ? 1.5 : 3), 'padding-right': theme.spacing(isMobile ? 1.5 : 3), }, diff --git a/src/config/index.js b/src/config/index.js index 7195333..2abb408 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -81,6 +81,12 @@ const notifications = { maxSnack: isMobile ? 3 : 4, }; +const loader = { + // no more blinking in your app + delay: 300, // if your asynchronous process is finished during 300 milliseconds you will not see the loader at all + minimumLoading: 700, // but if it appears, it will stay for at least 700 milliseconds +}; + export { messages, cancelationMessage, @@ -89,6 +95,7 @@ export { email, domain, repository, + loader, title, themePair, notifications, diff --git a/src/routes/index.js b/src/routes/index.js index ee6af32..fb0036e 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -1,35 +1,33 @@ -import { lazy } from 'react'; - -import AsyncComponentLoader from 'components/AsyncComponentLoader'; +import { asyncComponentLoader } from 'utils'; const routes = [ { exact: true, - component: AsyncComponentLoader(lazy(() => import('pages/Welcome'))), + component: asyncComponentLoader(_ => import('pages/Welcome')), path: '/', }, { exact: true, - component: AsyncComponentLoader(lazy(() => import('pages/Page1'))), + component: asyncComponentLoader(_ => import('pages/Page1')), path: '/page-1', }, { exact: true, - component: AsyncComponentLoader(lazy(() => import('pages/Page2'))), + component: asyncComponentLoader(_ => import('pages/Page2')), path: '/page-2', }, { exact: true, - component: AsyncComponentLoader(lazy(() => import('pages/Page3'))), + component: asyncComponentLoader(_ => import('pages/Page3')), path: '/page-3', }, { exact: true, - component: AsyncComponentLoader(lazy(() => import('pages/Page4'))), + component: asyncComponentLoader(_ => import('pages/Page4')), path: '/page-4', }, { - component: AsyncComponentLoader(lazy(() => import('components/NotFound'))), + component: asyncComponentLoader(_ => import('components/NotFound')), }, ]; diff --git a/src/utils/asyncComponentLoader.js b/src/utils/asyncComponentLoader.js new file mode 100644 index 0000000..6d82bf0 --- /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 loading process +// takes less than 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 again lead to blinking on the page. + +// The solution of the second problem is to set of a minimum timeout, which will +// ensure that the falback component will be rendered for that minimum amount of time + +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 these two (plus some "magic" plus some backflips), +// will secure 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; diff --git a/src/utils/index.js b/src/utils/index.js index c0e95e9..7bff4ca 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -3,6 +3,8 @@ import noop from './noop'; import isMobile from './isMobile'; import resetApp from './resetApp'; import downloadFile from './downloadFile'; +import sleep from './sleep'; +import asyncComponentLoader from './asyncComponentLoader'; export { today, @@ -10,4 +12,6 @@ export { isMobile, resetApp, downloadFile, + sleep, + asyncComponentLoader, }; diff --git a/src/utils/sleep.js b/src/utils/sleep.js new file mode 100644 index 0000000..a56adec --- /dev/null +++ b/src/utils/sleep.js @@ -0,0 +1,3 @@ +const sleep = ms => new Promise(res => setTimeout(res, ms)); + +export default sleep; diff --git a/src/utils/today.js b/src/utils/today.js index 3dd4906..3ff3730 100644 --- a/src/utils/today.js +++ b/src/utils/today.js @@ -2,8 +2,6 @@ import dayjs from 'dayjs'; import { dateFormat } from 'config'; -const today = _ => { - return dayjs().format(dateFormat); -}; +const today = _ => dayjs().format(dateFormat); export default today;