'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 |