Fallback component animations with Suspense
and lazy
@Akrom Sprinter has a good solution in case of fast load times, as it hides the fallback spinner and avoids overall delay. Here is an extension for more complex animations requested by OP:
1. Simple variant: fade-in + delayed display
const App = () => { const [isEnabled, setEnabled] = React.useState(false); return ( <div> <button onClick={() => setEnabled(b => !b)}>Toggle Component</button> <React.Suspense fallback={<Fallback />}> {isEnabled && <Home />} </React.Suspense> </div> );};const Fallback = () => { const containerRef = React.useRef(); return ( <p ref={containerRef} className="fallback-fadein"> <i className="fa fa-spinner spin" style={{ fontSize: "64px" }} /> </p> );};const Home = React.lazy(() => fakeDelay(2000)(import_("./routes/Home")));function import_(path) { return Promise.resolve({ default: () => <p>Hello Home!</p> });}function fakeDelay(ms) { return promise => promise.then( data => new Promise(resolve => { setTimeout(() => resolve(data), ms); }) );}ReactDOM.render(<App />, document.getElementById("root"));
.fallback-fadein { visibility: hidden; animation: fadein 1.5s; animation-fill-mode: forwards; animation-delay: 0.5s; }@keyframes fadein { from { visibility: visible; opacity: 0; } to { visibility: visible; opacity: 1; }}.spin { animation: spin 2s infinite linear;}@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(359deg); }}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css"/><div id="root"></div>
You just add some @keyframes
animations to Fallback
component, and delay its display either by setTimeout
and a state flag, or by pure CSS (animation-fill-mode
and -delay
used here).
2. Complex variant: fade-in and out + delayed display
This is possible, but needs a wrapper. We don't have a direct API for Suspense
to wait for a fade out animation, before the Fallback
component is unmounted.
Let's create a custom useSuspenseAnimation
Hook, that delays the promise given to React.lazy
long enough, so that our ending animation is fully visible:
const DeferredHomeComp = React.lazy(() => Promise.all([ import("./routes/Home"), deferred.promise ]).then(([imp]) => imp))
const App = () => { const { DeferredComponent, ...fallbackProps } = useSuspenseAnimation( "./routes/Home" ); const [isEnabled, setEnabled] = React.useState(false); return ( <div> <button onClick={() => setEnabled(b => !b)}>Toggle Component</button> <React.Suspense fallback={<Fallback {...fallbackProps} />}> {isEnabled && <DeferredComponent />} </React.Suspense> </div> );};const Fallback = ({ hasImportFinished, enableComponent }) => { const ref = React.useRef(); React.useEffect(() => { const current = ref.current; current.addEventListener("animationend", handleAnimationEnd); return () => { current.removeEventListener("animationend", handleAnimationEnd); }; function handleAnimationEnd(ev) { if (ev.animationName === "fadeout") { enableComponent(); } } }, [enableComponent]); const classes = hasImportFinished ? "fallback-fadeout" : "fallback-fadein"; return ( <p ref={ref} className={classes}> <i className="fa fa-spinner spin" style={{ fontSize: "64px" }} /> </p> );};function useSuspenseAnimation(path) { const [state, setState] = React.useState(init); const enableComponent = React.useCallback(() => { if (state.status === "IMPORT_FINISHED") { setState(prev => ({ ...prev, status: "ENABLED" })); state.deferred.resolve(); } }, [state]); return { hasImportFinished: state.status === "IMPORT_FINISHED", DeferredComponent: state.DeferredComponent, enableComponent }; function init() { const deferred = deferPromise(); const DeferredComponent = React.lazy(() => Promise.all([ fakeDelay(2000)(import_(path)).then(imp => { setState(prev => ({ ...prev, status: "IMPORT_FINISHED" })); return imp; }), deferred.promise ]).then(([imp]) => imp) ); return { status: "LAZY", DeferredComponent, deferred }; }}function import_(path) { return Promise.resolve({ default: () => <p>Hello Home!</p> });}function fakeDelay(ms) { return promise => promise.then( data => new Promise(resolve => { setTimeout(() => resolve(data), ms); }) );}function deferPromise() { let resolve; const promise = new Promise(_resolve => { resolve = _resolve; }); return { resolve, promise };}ReactDOM.render(<App />, document.getElementById("root"));
.fallback-fadein { visibility: hidden; animation: fadein 1.5s; animation-fill-mode: forwards; animation-delay: 0.5s; }@keyframes fadein { from { visibility: visible; opacity: 0; } to { visibility: visible; opacity: 1; }}.fallback-fadeout { animation: fadeout 1s; animation-fill-mode: forwards;}@keyframes fadeout { from { opacity: 1; } to { opacity: 0; }}.spin { animation: spin 2s infinite linear;}@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(359deg); }}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css"/><div id="root"></div>
Key points for complex variant
1.) useSuspenseAnimation
Hook returns three values:
hasImportFinished
(boolean
) → if true
, Fallback
can start its fade out animationenableComponent
(callback) → invoke it to unmount Fallback
, when animation is done.DeferredComponent
→ extended lazy Component loaded by dynamic import
2.) Listen to the animationend
DOM event, so we know when animation has ended.