Skip to content

Commit

Permalink
Merge pull request #3 from suren-atoyan/delayed-fallback
Browse files Browse the repository at this point in the history
Delayed fallback
  • Loading branch information
suren-atoyan authored Jun 30, 2020
2 parents fef9447 + 129838e commit a18a82a
Show file tree
Hide file tree
Showing 9 changed files with 138 additions and 28 deletions.
13 changes: 0 additions & 13 deletions src/components/AsyncComponentLoader/Component.js

This file was deleted.

3 changes: 0 additions & 3 deletions src/components/AsyncComponentLoader/index.js

This file was deleted.

1 change: 1 addition & 0 deletions src/components/Page/styles.js
Original file line number Diff line number Diff line change
Expand Up @@ -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),
},
Expand Down
7 changes: 7 additions & 0 deletions src/config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -89,6 +95,7 @@ export {
email,
domain,
repository,
loader,
title,
themePair,
notifications,
Expand Down
16 changes: 7 additions & 9 deletions src/routes/index.js
Original file line number Diff line number Diff line change
@@ -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')),
},
];

Expand Down
115 changes: 115 additions & 0 deletions src/utils/asyncComponentLoader.js
Original file line number Diff line number Diff line change
@@ -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

/*
<Suspense fallback={<Loading />}>
{lazy(_ => import('path/to/the/component'))}
</Suspense>
*/

// 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 && <LoadingComponent {...props} />;
}

/* ================================================================================== */

// 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 (
<Suspense fallback={<Fallback {...loadingProps} />}>
<LazyComponent {...props} />
</Suspense>
);
};

export default asyncComponentLoader;
4 changes: 4 additions & 0 deletions src/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@ 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,
noop,
isMobile,
resetApp,
downloadFile,
sleep,
asyncComponentLoader,
};
3 changes: 3 additions & 0 deletions src/utils/sleep.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const sleep = ms => new Promise(res => setTimeout(res, ms));

export default sleep;
4 changes: 1 addition & 3 deletions src/utils/today.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

0 comments on commit a18a82a

Please sign in to comment.