Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
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;
}
@AlexGalays

This comment has been minimized.

Copy link

commented Nov 9, 2018

No need to do that check {results &&
as you init the value with an empty array (which is nice)

@reddhouse

This comment has been minimized.

Copy link

commented Nov 10, 2018

Appreciate this example. As written, setResults(filteredResults) should be changed to setResults(results.data.results). Also consider using a key in your results.map(). Marvel provides a nice unique result.id you could use.

@gragland

This comment has been minimized.

Copy link
Owner Author

commented Nov 10, 2018

@AlexGalays @reddhouse All good suggestions, thanks! Updated the code :)

@taranvohra

This comment has been minimized.

Copy link

commented Mar 13, 2019

I feel theres a tiny bug here,

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

Let's say 2 API calls are made,
Call 1 at time X
Call 2 at time Y, (Y is greater than X)
If Call 2 resolves early and Call 1 resolves late, the latest results in the view will be of Call 1 whereas it should've been of Call 2. Here we need to commit only that result which was latest.

One way would be to use a ref,

 const latest = React.useRef(null);
  ...
  useEffect(
    () => {
      if (debouncedSearchTerm) {
        let now = latest.current = Date.now()
        setIsSearching(true);
        searchCharacters(debouncedSearchTerm).then(results => {
          if(now === latest.current) {
             setIsSearching(false);
             setResults(results);
          }
        });
      } else {
        setResults([]);
      }
    },
    [debouncedSearchTerm] // Only call effect if debounced search term changes
  );
@ArthurClemens

This comment has been minimized.

Copy link

commented Mar 15, 2019

Input's onChange is triggered by pressing Enter, which obviates the need for a debounce function. Using onInput makes more sense.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.