Skip to content

Instantly share code, notes, and snippets.

@gragland
Last active March 21, 2022 19:42
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save gragland/e50346f02e7edf4f81cc0bda33d3cae6 to your computer and use it in GitHub Desktop.
Save gragland/e50346f02e7edf4f81cc0bda33d3cae6 to your computer and use it in GitHub Desktop.
import { useState, useEffect, useRef } from 'react';
// Usage
function App() {
// State and setters for ...
// Search term
const [searchTerm, setSearchTerm] = useState('');
// API search results
const [results, setResults] = useState([]);
// Searching status (whether there is pending API request)
const [isSearching, setIsSearching] = useState(false);
// Debounce search term so that it only gives us latest value ...
// ... if searchTerm has not been updated within last 500ms.
// The goal is to only have the API call fire when user stops typing ...
// ... so that we aren't hitting our API rapidly.
const debouncedSearchTerm = useDebounce(searchTerm, 500);
// Effect for API call
useEffect(
() => {
if (debouncedSearchTerm) {
setIsSearching(true);
searchCharacters(debouncedSearchTerm).then(results => {
setIsSearching(false);
setResults(results);
});
} else {
setResults([]);
}
},
[debouncedSearchTerm] // Only call effect if debounced search term changes
);
return (
<div>
<input
placeholder="Search Marvel Comics"
onChange={e => setSearchTerm(e.target.value)}
/>
{isSearching && <div>Searching ...</div>}
{results.map(result => (
<div key={result.id}>
<h4>{result.title}</h4>
<img
src={`${result.thumbnail.path}/portrait_incredible.${
result.thumbnail.extension
}`}
/>
</div>
))}
</div>
);
}
// API search function
function searchCharacters(search) {
const apiKey = 'f9dfb1e8d466d36c27850bedd2047687';
return fetch(
`https://gateway.marvel.com/v1/public/comics?apikey=${apiKey}&titleStartsWith=${search}`,
{
method: 'GET'
}
)
.then(r => r.json())
.then(r => r.data.results)
.catch(error => {
console.error(error);
return [];
});
}
// Hook
function useDebounce(value, delay) {
// State and setters for debounced value
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(
() => {
// Update debounced value after delay
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
// Cancel the timeout if value changes (also on delay change or unmount)
// This is how we prevent debounced value from updating if value is changed ...
// .. within the delay period. Timeout gets cleared and restarted.
return () => {
clearTimeout(handler);
};
},
[value, delay] // Only re-call effect if value or delay changes
);
return debouncedValue;
}
@MavenOfCode
Copy link

It also will no longer render because the Marvel API returns a 401 with the following:
{"code":"MissingParameter","message":"You must provide a hash."}

Thanks for the example though!

@OfirLana
Copy link

OfirLana commented Feb 1, 2020

TypeError
Cannot read property 'results' of undefined

@adam-clason
Copy link

It seems that the setDebouncedValue function is still called if value changes since the timer started. The cleanup function does clear the timeout in some scenarios, but that only runs if the containing component is unmounted AFAIK. I was seeing the debouncedValue state being updated for every value change, just with the delay. I ended up tweaking useDebounce a bit and it seems to work. Something like this: (apologize for the typescript if that's not your thing)

const useDebounce = <T>(value: T, delay: number): T => {
    const [debouncedValue, setDebouncedValue] = useState<T>(value);
    const handlerRef = useRef<NodeJS.Timeout>();

    useEffect(() => {
        // If there is an active debounce timer, clear it since 
        // we now have a more up to date value.
        if (handlerRef.current) {
            clearTimeout(handlerRef.current);
        }

        handlerRef.current = setTimeout(() => {    
            setDebouncedValue(value);
        }, delay);

        return () => handlerRef.current && clearTimeout(handlerRef.current);
    }, [value]);
    
    return debouncedValue;
};

export default useDebounce;

@scscgit
Copy link

scscgit commented Jul 7, 2020

For a reference, here's an alternative implementation at @react-hook/debounce, along with a description why it's supposedly superior: jaredLunde/react-hook#35

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment