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; | |
} |
This comment has been minimized.
This comment has been minimized.
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. |
This comment has been minimized.
This comment has been minimized.
@AlexGalays @reddhouse All good suggestions, thanks! Updated the code :) |
This comment has been minimized.
This comment has been minimized.
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, One way would be to use a 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
); |
This comment has been minimized.
This comment has been minimized.
Input's |
This comment has been minimized.
This comment has been minimized.
I have tried to replicate the original import React, { useRef, useEffect, useState } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
/**
* @description should display log message after 1 second
*/
function useDebounce(callback, time, immediate) {
let args;
const id = useRef();
const initialState = useRef();
initialState.current = true;
const debounceFunction = () => {
args = arguments;
};
useEffect(() => {
if (!initialState.current) {
const callNow = immediate && !id.current;
const executeFuncLater = () => {
id.current = null;
if (!immediate) callback(args);
};
if (callNow) callback(args);
id.current = setTimeout(executeFuncLater, time);
}
return () => {
initialState.current = false;
clearTimeout(id.current);
};
});
return debounceFunction;
}
function App() {
const [text, setText] = useState();
const debounceFunction = useDebounce(
() => {
console.log("text", text);
},
500,
true
);
const handleOnClick = () => {
setText(text => text + "test1");
debounceFunction(text);
};
return (
<div className="App">
<button onClick={handleOnClick}>Click Me</button>
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<h2>{text}</h2>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement); Lemme know your feedback PS: Works fine for a click example.....now making it work with the search example in the docs |
This comment has been minimized.
This comment has been minimized.
The current sandbox of useDebounce does not render the image because it is missing an accessibilityLabel (alt): img elements must have an alt prop, either with meaningful text, or an empty string for decorative images. (jsx-a11y/alt-text) eslint This is in Chrome Version 76.0.3809.100. |
This comment has been minimized.
This comment has been minimized.
It also will no longer render because the Marvel API returns a 401 with the following: Thanks for the example though! |
This comment has been minimized.
This comment has been minimized.
TypeError |
This comment has been minimized.
This comment has been minimized.
It seems that the
|
This comment has been minimized.
This comment has been minimized.
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 |
This comment has been minimized.
No need to do that check
{results &&
as you init the value with an empty array (which is nice)