Do tej pory w projektach które tworzyłem do zarządzania stanem używany był głównie Redux. Lądowały w nim dane z serwera, stan urządzeń jak np wybrana przez użytkownika kamera i mikrofon czy różne dane potrzebne w wielu miejscach aplikacji jak uprawnienia użytkownika w systemie.
Aplikacje te miały dość rozbudowany stan kliencki, do przechowywania którego Redux jest stworzony. Stan „serwera”, czyli dane naszej aplikacji jest inny w swoim zachowaniu. Zmienia się bez wiedzy frontendu, może być z jakiegoś powodu niedostępny, łatwo też stracić synchronizację z danymi z serwera, które mogą się zmieniać w trakcie pracy klienta.
Stąd, jeśli stan aplikacji to głównie stan serwera a nie stan kliencki, rozsądne wydaje się zastosowanie biblioteki React Query. Zatroszczy się ona o cache danych, przechowywanie w aplikacji i aktualizację na serwerze.
Fejkowe API dzięki ServiceWorker
By pokazać niektóre funkcjonalności React Query w całej okazałości, będziemy potrzebować wprowadzić API które będzie przetrzymywać zmiany przez nas wprowadzone – przynajmniej w zakresie sesji przeglądarki.
Możnaby użyć jakiegoś prostego serwera, jednak można również użyć ServiceWorkera który będzie reagował na wysłane przez Fetch API requesty.
Takie rozwiązanie ma jednak jedną wadę – musimy odświeżyć stronę w sytuacji, gdy worker się nie zamontuje, co może mieć miejsce np podczas odświeżenia strony z CTRL + F5:
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
if(!navigator.serviceWorker.controller){
returnwindow.location.reload();
}
if (!navigator.serviceWorker.controller) {
return window.location.reload();
}
if (!navigator.serviceWorker.controller) {
return window.location.reload();
}
Nie jest to jednak problem podczas lokalnego developmentu gdzie przeładowanie będzie szybkie.
Za każdym razem gdy aplikacja wyśle żądanie przez fetch, jeśli adres zapytania będzie zawierał 'apis’ odpowiedź zostanie stworzona w funkcji getResponse i przekazana jako zwrotka do aplikacji:
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
let posts = [];
const getResponse = (request)=>{
if(request.url.endsWith('apis')){
returnnewResponse(JSON.stringify({ posts }));
}
if(request.url.endsWith('apis/add')){
request.json().then(post => posts = [
...posts,
{
...post,
id: newDate().getTime(),
},
])
returnnewResponse('');
}
}
let posts = [];
const getResponse = (request) => {
if (request.url.endsWith('apis')) {
return new Response(JSON.stringify({ posts }));
}
if (request.url.endsWith('apis/add')) {
request.json().then(post => posts = [
...posts,
{
...post,
id: new Date().getTime(),
},
])
return new Response('');
}
}
let posts = [];
const getResponse = (request) => {
if (request.url.endsWith('apis')) {
return new Response(JSON.stringify({ posts }));
}
if (request.url.endsWith('apis/add')) {
request.json().then(post => posts = [
...posts,
{
...post,
id: new Date().getTime(),
},
])
return new Response('');
}
}
Samo wysyłanie zapytań wyglądać będzie następująco:
Flaga isFetching od flagi isLoading różni się tym, że ta druga jest ustawiana na true tylko podczas pierwszego załadowania danych.
Ważną funkcjonalnością biblioteki jest to, że podczas ładowania nowych danych zwracane są dane poprzednie. Dlatego też potrzebowaliśmy API przetrzymującego stan Podczas dodawania postu, mimo iż wyświetla się informacja o ładowaniu danych, poprzednie posty po dodaniu nowego są cały czas wyświetlane:
useMutation w liście postów
Aby wymusić na bibliotece odświeżenie danych, zdeklarujemy to w mutacji dodającej nowego posta:
Funkcja invalidateQueries jako parametr przyjmuje nazwę query które chcemy zinwalidować oraz opcje. Potrzebujemy użyć flagi exact, nie zostały zinwalidowane inne zapytania które zaczynają się od słowa 'posts’.
Do samego wysłania requestu na backend posłuży nam funkcja onSubmit. Funkcja ta użyje mutate zwróconego przez hook useMutation:
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
const onSubmit = e =>{
e.preventDefault();
mutate(post);
};
const onSubmit = e => {
e.preventDefault();
mutate(post);
};
const onSubmit = e => {
e.preventDefault();
mutate(post);
};
Dzięki funkcji reset również zwróconej z hooka useMutation możemy pozbyć się informacji mówiącej o sukcesie wysłania requestu na serwer:
const createOnnChange = key => e => {
if (isSuccess) {
reset();
}
setPost(post => ({...post, [key]: e.target.value}));
};
const createOnnChange = key => e => {
if (isSuccess) {
reset();
}
setPost(post => ({...post, [key]: e.target.value}));
};
Dzięki temu flaga isSuccess zostanie przywrócona do początkowego stanu i zniknie informacja na widoku. Co ważne, reset nie spowoduje przeładowania listy postów.
Hook useQuery przyjmuje nazwę identyfikującą dany hook bądź listę elementów, która pozwoli go zidentyfikować. W naszym przypadku id posta jest unikalne, więc wystarczy ono aby biblioteka rozróżniła komentarze dla poszczególnych postów.
Analogicznie jak w przykładzie dla postów, nad listą komentarzy będziemy wyświetlać informację o ładowaniu danych:
Dzięki przyciskowi Show comments możesz doładować komentarze dla danego posta, dla widocznych komentarzy pokaże się przycisk Hide comments. Pozwoli to potwierdzić, że biblioteka przetrzymuje poprzednie dane pomiedzy rerenderami – klikając przycisk zauważysz w konsoli że zapytanie jest wysyłane, jednak po pierwszym załadowaniu dane są uaktualniane i wyświetlone po tym, jak zapytanie się skończy.
Ważne by pamiętać o zinwalidowaniu listy komentarzy dla danego posta oraz o tym, by do useMutation również przekazać id postu, by np reset resetował tylko to zapytanie.
Reset również odbywa się przy rozpoczęciu wpisywania danych nowego komentarza:
Biblioteka React Query umożliwia bardzo sprawne zarządzanie stanem serwerowym w aplikacji, dzięki między innymi inwalidowaniu zapytań i wyświetlaniu poprzednich danych zanim załadują się nowe, co poprawia UX. Warto więc ją poznać i stosować, gdy samego stanu czysto klienckiego w aplikacji mamy mało.