I use this React hook to reduce the display of spinner animation icons in cases where a little patience means we don't really need to bother indicating that something is loading.
The problem
For example, you might have a TanStack Query that makes XHR requests to the backend, and that backend is generally fast. Many times, it finishes in tens or hundreds of milliseconds. If you prescriptively always show the loading spinner icon, or whatever you might have, it's going to flicker and cause confusion.
For example, your use of useQuery might look like this:
function MyComponent() {
const { data, isPending, error } = useQuery({
queryKey: ["something"],
queryFn: fetcher,
})
return <div>
{isPending && <Spinner/>}
{error && <Alert error={error}/>}
{data && <Tabular data={data} />}
</div>
}
The problem is that if isPending is only true for a very short time, you run the risk of displaying the <Spinner/> so briefly that it just becomes a flickering blur to the user.
The solution
The hook code looks like this:
import { useEffect, useState } from "react";
type Options = {
delay?: number;
};
/**
* A hook that throttles the truth. Useful when you want something to be true
* only if it's been true for a certain delay in milliseconds. Example use:
*
* const stillLoading = useSlowTruth(isLoading);
*
* If the value of `isLoading` quickly changes from false, to true, to false;
* the value of `stillLoading` will remain false the whole time.
*
* @param initialState boolean
* @param options
* @returns a single boolean that is a delayed mirror of the input, if it's true
*/
export function useSlowTruth(initialState: boolean, { delay = 1000 }: Options) {
const [isTrue, setIsTrue] = useState(initialState);
useEffect(() => {
let mounted = true;
let timer: number | null = null;
if (initialState) {
timer = window.setTimeout(() => {
if (mounted) {
setIsTrue(true);
}
}, delay);
} else {
if (timer !== null) {
window.clearTimeout(timer);
}
setIsTrue(false);
}
return () => {
if (timer !== null) {
window.clearTimeout(timer);
}
mounted = false;
};
}, [initialState, delay]);
return isTrue;
}
I put together a demo app here: https://github.com/peterbe/use-slow-truth-demo
The usage
This change
+import { useSlowTruth } from "./useSlowTruth"
+
function MyComponent() {
const { data, isPending, error } = useQuery({
queryKey: ["something"],
queryFn: fetcher,
})
+ const isStillPending = useSlowTruth(isPending, { delay: 300 })
return <div>
- {isPending && <Spinner/>}
+ {isStillPending && <Spinner/>}
{error && <Alert error={error}/>}
{data && <Tabular data={data} />}
</div>
}
Now, only if the XHR query takes longer than 300ms does it show the <Spinner/> component.