Rozsynchronizowanie inputa z listą pochodzącą z api
24 February 2020
Wstęp
Wykonując zapytania do api na podstawie inputu tekstowego użytkownika łatwo doprowadzić do sytuacji, w której rozsynchronizujemy wyświetlaną listę oraz input. W tym artykule przedstawię z czego to może wynikać oraz jakie rozwiązania możemy zastosować.
Problem
Wybraźmy sobie następującą sytuację. Mamy na stronie input, który pozwala na na filtrowanie listy. Ze względu na to, że lista jest duża, nie chcemy ściągać jej całej tylko przy każdej zmianie wykonujemy zapytanie do api.
Jeżeli przy każdej zmianie będziemy wykonywać zapytanie łatwo o sytuację, gdzie nasz tekst w inpucie będzie rozsynchronizowany względem otrzymanej odpowiedzi.
Załóżmy, że użytkownik wpisał "jozek". Do api poleci 5 zapytań:
- api/?name=j
- api/?name=jo
- api/?name=joz
- api/?name=joze
- api/?name=jozek
Niestety nic nie gwarantuje nam, że otrzymamy odpowiedzi w tej samej kolejności! Ze względu na magię internetu odpowiedzi mogą wrócić w nastepującej kolejności:
- api/?name=j
- api/?name=joz
- api/?name=jo
- api/?name=jozek
- api/?name=joze
W tej sytuacji, w naszym polu tekstowym będziemy mieli wpisane "jozek", natomiast lista będzie wyświetlać listę dla "joze".
Jakie mamy rozwiązania tej sytuacji?
Rozwiązania
Debounce
Debounce to technika polegająca na wykonaniu akcji dopiero gdy minie określony czas od ostatniej zmiany. Stosujemy go gdy nie chcemy wywoływać jakieś funkcji zbyt często.
Przykładowo, jeśli nasz użytkownik wpisuje kolejny znak co 200ms, a my ustawimy czas oczekiwania na 500ms zamiast wykonać 5 zapytań wykonamy tylko 1. Modyfikując powyższy przykład:
- (0ms) wartość inputa =j
- (200ms) wartość inputa =jo
- (400ms) wartość inputa =joz
- (600ms) wartość inputa =joze
- (800ms) wartość inputa =jozek
- (1300ms)zapytanie do api/?name=jozek
Zatem zamiast wykonać 5 zapytań wykonamy tylko jedno dopiero gdy użytkownik skończy pisać.
Poniżej przykładowa implementacja. Możesz też skorzystać z lodasha
const debounce = (callback, time) => {
let timer;
return () => {
// jeśli funkcja nie zdążyła się wywołać to usuń timer
clearTimeout(timer);
// ustaw by po określonym czasie funkcja się wywołała
timer = setTimeout(callback, time);
};
};
Rozwiązanie to ma jednak parę wad:
- użytkownicy mają różne tempo pisania, dla niektórych to nic nie da bo i tak piszą wolno natomiast dla reszty strona bedzie wydawała się mniej responsywna(będzie wolniej reagować)
- musimy próbować odgadnać jaki będzie najrozsądniejszy balans między szybkością (chcemy jak najszybciej odpytać api) oraz stabilnością (chcemy mieć dość czasu by uniknąć problemu z wyścigiem odpowiedzi z api)
Anulowanie wcześniejszych zapytań
Jeżeli korzystamy z biblioteki wspierającej anulowanie zapytań możemy wykorzystać tę funkcjonalność. Przykładowa implementacja poniżej, bazujac na odpowiedzi z issue na githubie.
export const createCancellableGetRequest = () => {
// tu będziemy trzymać referencję do poprzedniego zapytania (oraz tokenu do anulowania)
let call;
return function(url) {
// jak mamy jakiś token to ubijmy wcześniejsze zapytanie
if (call) {
call.cancel();
}
// zapisz token
call = axios.CancelToken.source();
// wykonaj zapytanie
return axiosInstance.get(url, { cancelToken: call.token });
};
};
const get = createCancellableGetRequest();
get('url').then();
Co ważne musimy potem obsłużyć dwie rzeczy:
- Anulowane zapytania będą zwracały błędy. Możemy to obsłużyć na poziomie interceptora
axiosInstance.interceptors.response.use(
response => {
return response;
},
error => {
if (axios.isCancel(error)) {
// błąd wyrzucany przy ubiciu zapytania
return;
}
- W wyniku powyższego nasze anulowane zapytania będą zwracać pusty response, co musimy obsłużyć by nie było typeerrorów!
Skorzystanie z observabli
Niestety nie mam jakiegoś sensownego przykładu pod ręką więc wrzucam pierwszy lepszy artykuł z neta
W skrócie zamiast reagować na zmiany tworzymy na nie subskrypcje. W momencie wystąpienia zmiany tworzone jest nowe zapytanie a wcześniejsze są ignorowane (lub anulowane).