React suspense/lazy delay?

后端 未结 5 1657
醉话见心
醉话见心 2020-12-05 18:13

I am trying to use the new React Lazy and Suspense to create a fallback loading component. This works great, but the fallback is showing only a few ms. Is there a way to add

相关标签:
5条回答
  • 2020-12-05 18:34

    lazy function is supposed to return a promise of { default: ... } object which is returned by import() of a module with default export. setTimeout doesn't return a promise and cannot be used like that. While arbitrary promise can:

    const Home = lazy(() => {
      return new Promise(resolve => {
        setTimeout(() => resolve(import("./home")), 300);
      });
    });
    

    If an objective is to provide minimum delay, this isn't a good choice because this will result in additional delay.

    A minimum delay would be:

    const Home = lazy(() => {
      return Promise.all([
        import("./home"),
        new Promise(resolve => setTimeout(resolve, 300))
      ])
      .then(([moduleExports]) => moduleExports);
    });
    
    0 讨论(0)
  • 2020-12-05 18:37

    As mentioned by loopmode, component fallback should have a timeout.

    import React, { useState, useEffect } from 'react'
    
    const DelayedFallback = () => {
      const [show, setShow] = useState(false)
      useEffect(() => {
        let timeout = setTimeout(() => setShow(true), 300)
        return () => {
          clearTimeout(timeout)
        }
      }, [])
    
      return (
        <>
          {show && <h3>Loading ...</h3>}
        </>
      )
    }
    export default DelayedFallback
    

    Then just import that component and use it as fallback.

    <Suspense fallback={<DelayedFallback />}>
           <LazyComponent  />
    </Suspense>
    
    0 讨论(0)
  • 2020-12-05 18:39

    I faced similar problem moreover I was using TypeScript along with React. So, I had to respect typescript compiler as well & I went ahead with an approach having an infinite delay along with no complain from typescript as well. Promise that never resolved

    0 讨论(0)
  • 2020-12-05 18:50

    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>
      );
    };
    
    /*
     Technical helpers
    */
    
    const Home = React.lazy(() => fakeDelay(2000)(import_("./routes/Home")));
    
    // import_ is just a stub for the stack snippet; use dynamic import in real code.
    function import_(path) {
      return Promise.resolve({ default: () => <p>Hello Home!</p> });
    }
    
    // add some async delay for illustration purposes
    function fakeDelay(ms) {
      return promise =>
        promise.then(
          data =>
            new Promise(resolve => {
              setTimeout(() => resolve(data), ms);
            })
        );
    }
    
    ReactDOM.render(<App />, document.getElementById("root"));
    /* Delay showing spinner first, then gradually let it fade in. */
    .fallback-fadein {
      visibility: hidden;
      animation: fadein 1.5s;
      animation-fill-mode: forwards;
      animation-delay: 0.5s; /* no spinner flickering for fast load times */
    }
    
    @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:

    // inside useSuspenseAnimation
    const DeferredHomeComp = React.lazy(() => Promise.all([
        import("./routes/Home"), 
        deferred.promise // resolve this promise, when Fallback animation is complete
      ]).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>
      );
    };
    
    /* 
    Possible State transitions: LAZY -> IMPORT_FINISHED -> ENABLED
    - LAZY: React suspense hasn't been triggered yet.
    - IMPORT_FINISHED: dynamic import has completed, now we can trigger animations.
    - ENABLED: Deferred component will now be displayed 
    */
    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();
        // component object reference  is kept stable, since it's stored in state.
        const DeferredComponent = React.lazy(() =>
          Promise.all([
            // again some fake delay for illustration
            fakeDelay(2000)(import_(path)).then(imp => {
              // triggers re-render, so containing component can react
              setState(prev => ({ ...prev, status: "IMPORT_FINISHED" }));
              return imp;
            }),
            deferred.promise
          ]).then(([imp]) => imp)
        );
    
        return {
          status: "LAZY",
          DeferredComponent,
          deferred
        };
      }
    }
    
    /*
    technical helpers
    */
    
    // import_ is just a stub for the stack snippet; use dynamic import in real code.
    function import_(path) {
      return Promise.resolve({ default: () => <p>Hello Home!</p> });
    }
    
    // add some async delay for illustration purposes
    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"));
    /* Delay showing spinner first, then gradually let it fade in. */
    .fallback-fadein {
      visibility: hidden;
      animation: fadein 1.5s;
      animation-fill-mode: forwards;
      animation-delay: 0.5s; /* no spinner flickering for fast load times */
    }
    
    @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 animation
    • enableComponent (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.

    0 讨论(0)
  • 2020-12-05 18:57

    You should create a fallback component that itself has a timeout and a visible state. Initially you set visible false. When fallback component gets mounted, it should setTimeout to turn visible state flag on. Either make sure your component is still mounted, or clear the timeout when the component gets unmounted. Finally, if visible state is false, render null in your fallback component (or e.g. just blocking/semi-transparent overlay but no spinner/animation)

    Then use such component, e.g. <Loading overlay/>, as fallback.

    0 讨论(0)
提交回复
热议问题