'React.js - Loading Indicator with a delay and anti-flickering

How can I show a loading indicator only when a loading state is true for more than 1s, but when it exceeds 1s and resolves before 2s show loading indicator for atleast 1s duration in React?

A similar question exists for Angular JS - which had these 5 conditions

  • If the data arrives successfully earlier than in 1 second, no indicator should be shown (and data should be rendered normally)
  • If the call fails earlier than in 1 second, no indicator should be shown (and error message should be rendered)
  • If the data arrives later than in 1 second an indicator should be shown for at least 1 second (to prevent flashing spinner, the data should be rendered afterwards)
  • If the call fails later than in 1 second an indicator should be shown for at least 1 second
  • If the call takes more than 10 seconds the call should be canceled (and error message displayed)

How can I achieve something similar but in React.js?

My custom hook:


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);
      });
  };

Alternatively, if I have a Loader component, can I add delay of 1s before it renders and do not unmount until 1s is completed?



Solution 1:[1]

React is just a library of rules, use normal javascript vanilla to achieve this, You can achieve your all scenarios using throttle mechanism. https://www.geeksforgeeks.org/lodash-_-throttle-method/

Solution 2:[2]

You can do something like the following (Sandbox):

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>
  );
}

Or using a custom lib set (Codesandbox demo):

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>
  );
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Siddharth Pachori
Solution 2