2019: prova gli hook + prometti il rimbalzo
Questa è la versione più aggiornata di come risolverei questo problema. Io userei:
Questo è un cablaggio iniziale ma stai componendo blocchi primitivi da solo e puoi creare il tuo gancio personalizzato in modo da doverlo fare solo una volta.
// Generic reusable hook
const useDebouncedSearch = (searchFunction) => {
// Handle the input text state
const [inputText, setInputText] = useState('');
// Debounce the original search async function
const debouncedSearchFunction = useConstant(() =>
AwesomeDebouncePromise(searchFunction, 300)
);
// The async callback is run each time the text changes,
// but as the search function is debounced, it does not
// fire a new request on each keystroke
const searchResults = useAsync(
async () => {
if (inputText.length === 0) {
return [];
} else {
return debouncedSearchFunction(inputText);
}
},
[debouncedSearchFunction, inputText]
);
// Return everything needed for the hook consumer
return {
inputText,
setInputText,
searchResults,
};
};
E poi puoi usare il tuo gancio:
const useSearchStarwarsHero = () => useDebouncedSearch(text => searchStarwarsHeroAsync(text))
const SearchStarwarsHeroExample = () => {
const { inputText, setInputText, searchResults } = useSearchStarwarsHero();
return (
<div>
<input value={inputText} onChange={e => setInputText(e.target.value)} />
<div>
{searchResults.loading && <div>...</div>}
{searchResults.error && <div>Error: {search.error.message}</div>}
{searchResults.result && (
<div>
<div>Results: {search.result.length}</div>
<ul>
{searchResults.result.map(hero => (
<li key={hero.name}>{hero.name}</li>
))}
</ul>
</div>
)}
</div>
</div>
);
};
Troverai questo esempio in esecuzione qui e dovresti leggere la documentazione di reattività asincrona per maggiori dettagli.
2018: prova a promettere il rimbalzo
Vogliamo spesso rimandare le chiamate API per evitare di inondare il back-end con richieste inutili.
Nel 2018, lavorare con i callback (Lodash / Underscore) mi sembra male e soggetto a errori. È facile imbattersi in problemi di plateplate e concorrenza dovuti alla risoluzione delle chiamate API in un ordine arbitrario.
Ho creato una piccola biblioteca con React in mente per risolvere i tuoi dolori: promessa di rimbalzo eccezionale .
Questo non dovrebbe essere più complicato di così:
const searchAPI = text => fetch('/search?text=' + encodeURIComponent(text));
const searchAPIDebounced = AwesomeDebouncePromise(searchAPI, 500);
class SearchInputAndResults extends React.Component {
state = {
text: '',
results: null,
};
handleTextChange = async text => {
this.setState({ text, results: null });
const result = await searchAPIDebounced(text);
this.setState({ result });
};
}
La funzione rimbalzata assicura che:
- Le chiamate API verranno rimosse
- la funzione rimbalzata restituisce sempre una promessa
- solo la promessa restituita dell'ultima chiamata verrà risolta
- un singolo
this.setState({ result });
accadrà per chiamata API
Alla fine, puoi aggiungere un altro trucco se il componente smonta:
componentWillUnmount() {
this.setState = () => {};
}
Si noti che Observables (RxJS) può anche adattarsi perfettamente al rimbalzo degli input, ma è un'astrazione più potente che può essere più difficile da imparare / usare correttamente.
<2017: vuoi ancora usare il callbo debouncing?
La parte importante qui è creare una singola funzione rimbalzata (o limitata) per istanza del componente . Non si desidera ricreare la funzione di debounce (o acceleratore) ogni volta e non si desidera che più istanze condividano la stessa funzione di debounce.
Non sto definendo una funzione di debouncing in questa risposta in quanto non è realmente pertinente, ma questa risposta funzionerà perfettamente con il carattere _.debounce
di sottolineatura o lodash, nonché con qualsiasi funzione di debouncing fornita dall'utente.
BUONA IDEA:
Poiché le funzioni debounce sono stateful, dobbiamo creare una funzione debounce per istanza del componente .
ES6 (proprietà di classe) : raccomandato
class SearchBox extends React.Component {
method = debounce(() => {
...
});
}
ES6 (costruttore di classi)
class SearchBox extends React.Component {
constructor(props) {
super(props);
this.method = debounce(this.method.bind(this),1000);
}
method() { ... }
}
ES5
var SearchBox = React.createClass({
method: function() {...},
componentWillMount: function() {
this.method = debounce(this.method.bind(this),100);
},
});
Vedi JsFiddle : 3 istanze producono 1 voce di registro per istanza (che ne rende 3 a livello globale).
Non è una buona idea:
var SearchBox = React.createClass({
method: function() {...},
debouncedMethod: debounce(this.method, 100);
});
Non funzionerà, perché durante la creazione della descrizione della classe dell'oggetto this
non è l'oggetto stesso creato. this.method
non restituisce ciò che ti aspetti perché il this
contesto non è l'oggetto stesso (che in realtà non esiste ancora BTW in quanto è appena stato creato).
Non è una buona idea:
var SearchBox = React.createClass({
method: function() {...},
debouncedMethod: function() {
var debounced = debounce(this.method,100);
debounced();
},
});
Questa volta stai effettivamente creando una funzione rimbalzata che chiama il tuo this.method
. Il problema è che lo stai ricreando ad ogni debouncedMethod
chiamata, quindi la funzione di rimbalzo appena creata non sa nulla delle chiamate precedenti! È necessario riutilizzare la stessa funzione rimbalzata nel tempo, altrimenti il rimbalzo non avverrà.
Non è una buona idea:
var SearchBox = React.createClass({
debouncedMethod: debounce(function () {...},100),
});
Questo è un po 'complicato qui.
Tutte le istanze montate della classe condivideranno la stessa funzione rimbalzata, e molto spesso questo non è quello che vuoi !. Vedi JsFiddle : 3 istanze stanno producendo solo 1 voce di registro a livello globale.
È necessario creare una funzione rimbalzata per ogni istanza del componente e non una singola funzione rimbalzata a livello di classe, condivisa da ciascuna istanza del componente.
Prenditi cura del pool di eventi di React
Ciò è dovuto al fatto che spesso vogliamo rimbalzare o limitare gli eventi DOM.
In React, gli oggetti evento (cioè, SyntheticEvent
) che si ricevono nei callback sono raggruppati (questo è ora documentato ). Ciò significa che dopo che è stato chiamato il callback dell'evento, il SyntheticEvent ricevuto verrà rimesso nel pool con attributi vuoti per ridurre la pressione GC.
Pertanto, se si accede alle SyntheticEvent
proprietà in modo asincrono al callback originale (come può accadere se si accelera / rimbalza), le proprietà a cui si accede potrebbero essere cancellate. Se si desidera che l'evento non venga mai rimesso nel pool, è possibile utilizzare il persist()
metodo
Senza persistere (comportamento predefinito: evento aggregato)
onClick = e => {
alert(`sync -> hasNativeEvent=${!!e.nativeEvent}`);
setTimeout(() => {
alert(`async -> hasNativeEvent=${!!e.nativeEvent}`);
}, 0);
};
Il secondo (asincrono) verrà stampato hasNativeEvent=false
perché le proprietà dell'evento sono state ripulite.
Con persistere
onClick = e => {
e.persist();
alert(`sync -> hasNativeEvent=${!!e.nativeEvent}`);
setTimeout(() => {
alert(`async -> hasNativeEvent=${!!e.nativeEvent}`);
}, 0);
};
Il 2 ° (asincrono) verrà stampato hasNativeEvent=true
perchépersist
consente di evitare di rimettere l'evento nel pool.
Puoi testare questi 2 comportamenti qui: JsFiddle
Leggi la risposta di Julen per un esempio di utilizzo persist()
con una funzione di accelerazione / debounce.
debounce
. qui, quandoonChange={debounce(this.handleOnChange, 200)}/>
, invocheràdebounce function
ogni volta. ma, in realtà, ciò di cui abbiamo bisogno è invocare la funzione restituita dalla funzione di debounce.