javascript
56 lines · 8 steps
Delaying a loading spinner with a React hook
A custom hook holds off showing a spinner until an operation runs long enough to warrant it, avoiding flicker on fast responses.
Explained by
highlit
1import { useEffect, useRef, useState } from 'react';
2
3export function useDelayedFlag(active, delay = 300) {
4 const [visible, setVisible] = useState(false);
5 const timeoutRef = useRef(null);
6
7 useEffect(() => {
8 if (active) {
9 timeoutRef.current = setTimeout(() => setVisible(true), delay);
10 } else {
11 setVisible(false);
12 }
13
14 return () => {
15 if (timeoutRef.current) {
16 clearTimeout(timeoutRef.current);
17 timeoutRef.current = null;
18 }
19 };
20 }, [active, delay]);
21
22 return visible;
23}
24
25export function UserSearchResults({ query }) {
26 const [results, setResults] = useState([]);
27 const [isFetching, setIsFetching] = useState(false);
28 const showSpinner = useDelayedFlag(isFetching, 250);
29
30 useEffect(() => {
31 if (!query) return;
32 const controller = new AbortController();
33 setIsFetching(true);
34
35 fetch(`/api/users?q=${encodeURIComponent(query)}`, { signal: controller.signal })
36 .then((res) => res.json())
37 .then((data) => setResults(data.users))
38 .catch((err) => {
39 if (err.name !== 'AbortError') throw err;
40 })
41 .finally(() => setIsFetching(false));
42
43 return () => controller.abort();
44 }, [query]);
45
46 return (
47 <div className="search-results">
48 {showSpinner && <Spinner label="Searching…" />}
49 <ul>
50 {results.map((user) => (
51 <li key={user.id}>{user.name}</li>
52 ))}
53 </ul>
54 </div>
55 );
56}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Delaying a loading indicator prevents distracting flicker when work finishes faster than the human eye needs.
- 2Storing timers and controllers in cleanup-aware effects keeps async work from leaking across renders.
- 3Extracting timing logic into a reusable hook keeps components focused on rendering rather than bookkeeping.
Related explainers
javascript
function evaluate(expression) { const tokens = tokenize(expression); let pos = 0;
Building a recursive descent calculator
parsing
recursion
operator-precedence
Intermediate
8 steps
javascript
import { useState, useEffect, useCallback, useRef } from 'react'; const cache = new Map(); const inflight = new Map();
Building a stale-while-revalidate hook in React
caching
request-deduplication
custom-hooks
Advanced
10 steps
typescript
import { useEffect, useState } from "react"; interface Section { id: string;
Building a scroll-spy hook in React
custom-hooks
intersectionobserver
dom-observation
Intermediate
8 steps
javascript
const SWIPE_THRESHOLD = 80; const MAX_TRANSLATE = 120; export function attachSwipeToDismiss(element, onDismiss) {
Building a swipe-to-dismiss gesture in JS
touch-events
gesture-detection
dom-manipulation
Intermediate
10 steps
javascript
const { pool } = require('./db'); function withTransaction() { return async (req, res, next) => {
A per-request transaction middleware in Express
middleware
database-transactions
connection-pooling
Advanced
7 steps
javascript
function collapseConsecutiveLogs(lines, { keyFn = (l) => l.message } = {}) { const groups = []; for (const line of lines) {
Collapsing consecutive log lines in JavaScript
grouping
run-length-encoding
data-transformation
Intermediate
7 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/delaying-a-loading-spinner-with-a-react-hook-explained-javascript-a15b/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.