首页 > 解决方案 > React.js - 具有延迟和防闪烁的加载指示器

问题描述

如何仅在加载状态超过1s 时才显示加载指示器,但是当它超过 1s 并在 2s 之前解决时,在 React 中显示加载指示器至少 1s 持续时间?

Angular JS 也存在类似的问题——它有这 5 个条件

  • 如果数据早于 1 秒到达成功,则不应显示任何指示符(应正常渲染数据)
  • 如果调用早于 1 秒失败,则不应显示任何指示符(应呈现错误消息)
  • 如果数据在 1 秒后到达,则指示器应显示至少 1 秒(为防止闪烁的微调器,应在之后呈现数据)
  • 如果呼叫在 1 秒后失败,则应显示至少 1 秒的指示符
  • 如果通话时间超过 10 秒,则应取消通话(并显示错误消息)

我怎样才能在 React.js 中实现类似的东西?

我的自定义钩子:


const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);

const next = async () => {
    setLoading(true);  //can i set this to be true 
                       //only if updateCurrent function takes more than 1s?

    updateCurrent(code)  //some async function
      .then(() => setLoading(false))  
      .catch((e) => {
        setLoading(false);
        setError(e);
      });
  };

或者,如果我有一个 Loader 组件,我可以在它呈现之前添加 1s 的延迟并且在 1s 完成之前不要卸载?

标签: javascriptreactjs

解决方案


您可以执行以下操作(沙箱):

import React, { useState, useRef, useEffect, useCallback } from "react";
import { Spinner } from "react-bootstrap";

export default function TestComponent(props) {
  const isMounted = useRef(true);

  const [text, setText] = useState("");
  const [isFetching, setIsFetching] = useState(false);
  const [showLoader, setShowLoader] = useState(false);

  useEffect(() => {
    return () => {
      isMounted.current = false;
    };
  }, []);

  const fetchJSON = useCallback((url) => {
    (async () => {
      let shown;

      const showTimer = setTimeout(() => {
        shown = true;
        isMounted.current && setShowLoader(true);
      }, 1000);

      const controller = new AbortController();

      const timeoutTimer = setTimeout(() => controller.abort(), 10000);

      try {
        setIsFetching(true);
        const response = await fetch(url, { signal: controller.signal });
        const json = await response.json();
        isMounted.current && setText(JSON.stringify(json));
      } catch (err) {
        isMounted.current && setText(err.toString());
      } finally {
        clearTimeout(timeoutTimer);
        isMounted.current && setIsFetching(false);
        if (shown) {
          setTimeout(() => {
            isMounted.current && setShowLoader(false);
          }, 1000);
        } else {
          clearTimeout(showTimer);
        }
      }
    })();
  }, []);

  return (
    <div className="component">
      <div className="caption">Demo:</div>
      <div>
        {showLoader ? <Spinner animation="border" variant="primary" /> : text}
      </div>
      <button
        className="btn btn-success"
        onClick={() => fetchJSON(props.url)}
        disabled={isFetching}
      >
        {isFetching ? "Fetching..." : "Fetch data"}
      </button>
    </div>
  );
}

或使用自定义库集(Codesandbox 演示):

import React, { useState } from "react";
import { useAsyncCallback, E_REASON_UNMOUNTED } from "use-async-effect2";
import { CPromise, CanceledError } from "c-promise2";
import cpAxios from "cp-axios";
import { ProgressBar } from "react-bootstrap";

export default function TestComponent(props) {
  const [text, setText] = useState("");
  const [progress, setProgress] = useState(0);
  const [isFetching, setIsFetching] = useState(false);
  const [showLoader, setShowLoader] = useState(false);

  const fetchUrl = useAsyncCallback(function* (options) {
    setIsFetching(true);
    setProgress(0);
    setText("");

    this.progress(setProgress);

    let loaderShown;

    const loaderPromise = CPromise.delay(1000).then(() => {
      loaderShown = true;
      setShowLoader(true);
    });

    try {
      this.innerWeight(2); // total weight for progress calculation
      const response = yield cpAxios(options).timeout(props.timeout);
      loaderPromise.cancel();
      yield CPromise.delay(3000); // just for fun
      setText(JSON.stringify(response.data));
    } catch (err) {
      loaderPromise.cancel();
      CanceledError.rethrow(err, E_REASON_UNMOUNTED);
      setText(err.toString());
    }

    setIsFetching(false);

    if (loaderShown) {
      yield CPromise.delay(1000);
      setShowLoader(false);
    }
  });

  return (
    <div className="component">
      <div className="caption">useAsyncEffect demo:</div>
      <div>{showLoader ? <ProgressBar now={progress * 100} /> : text}</div>
      {!isFetching ? (
        <button
          className="btn btn-success"
          onClick={() => fetchUrl(props.url)}
          disabled={isFetching}
        >
          Fetch data
        </button>
      ) : (
        <button
          className="btn btn-warning"
          onClick={() => fetchUrl.cancel()}
          disabled={!isFetching}
        >
          Cancel request
        </button>
      )}
    </div>
  );
}

推荐阅读