Pobieranie danych nieaktualnych podczas ponownego sprawdzania poprawności za pomocą haków React: przewodnik

Opublikowany: 2022-03-11

Wykorzystanie rozszerzenia Cache-Control „przestarzały podczas ponownego sprawdzania poprawności” jest popularną techniką. Polega na użyciu zbuforowanych (nieaktualnych) zasobów, jeśli zostaną znalezione w pamięci podręcznej, a następnie ponownej weryfikacji pamięci podręcznej i aktualizacji jej nowszą wersją zasobu, jeśli to konieczne. Stąd nazwa stale-while-revalidate .

Jak działa stale-while-revalidate

Gdy żądanie jest wysyłane po raz pierwszy, jest buforowane przez przeglądarkę. Następnie, gdy to samo żądanie jest wysyłane po raz drugi, najpierw sprawdzana jest pamięć podręczna. Jeśli pamięć podręczna tego żądania jest dostępna i poprawna, pamięć podręczna jest zwracana jako odpowiedź. Następnie pamięć podręczna jest sprawdzana pod kątem nieaktualności i jest aktualizowana, jeśli zostanie uznana za nieaktualną. Nieaktualność pamięci podręcznej jest określana przez wartość max-age obecną w nagłówku Cache-Control wraz z stale-while-revalidate .

Schemat blokowy śledzący logikę nieaktualności podczas ponownej walidacji. Zaczyna się od prośby. Jeśli nie jest buforowany lub pamięć podręczna jest nieprawidłowa, żądanie jest wysyłane, zwracana jest odpowiedź, a pamięć podręczna jest aktualizowana. W przeciwnym razie zwracana jest odpowiedź w pamięci podręcznej, po czym pamięć podręczna jest sprawdzana pod kątem nieaktualności. Jeśli jest nieaktualny, wysyłane jest żądanie, a pamięć podręczna jest aktualizowana.

Pozwala to na szybkie wczytywanie stron, ponieważ buforowane zasoby nie znajdują się już na ścieżce krytycznej. Są ładowane natychmiast. Ponadto, ponieważ programiści kontrolują częstotliwość używania i aktualizowania pamięci podręcznej, mogą uniemożliwić przeglądarkom wyświetlanie nadmiernie nieaktualnych danych użytkownikom.

Czytelnicy mogą pomyśleć, że jeśli mogą sprawić, by serwer używał pewnych nagłówków w swoich odpowiedziach i pozwolić przeglądarce pobrać je stamtąd, to po co używać Reacta i hooków do buforowania?

Okazuje się, że podejście serwer i przeglądarka działa dobrze tylko wtedy, gdy chcemy buforować zawartość statyczną. A co z używaniem stale-while-revalidate dynamicznego interfejsu API? W takim przypadku trudno jest wymyślić dobre wartości dla max-age i stale-while-revalidate . Często najlepszym rozwiązaniem jest unieważnienie pamięci podręcznej i pobieranie nowej odpowiedzi za każdym razem, gdy wysyłane jest żądanie. To skutecznie oznacza brak buforowania. Ale z React i Hooks możemy zrobić lepiej.

stale-while-revalidate dla API

Zauważyliśmy, że stale-while-revalidate HTTP nie działa dobrze z żądaniami dynamicznymi, takimi jak wywołania API.

Nawet jeśli w końcu go użyjemy, przeglądarka zwróci albo pamięć podręczną, albo nową odpowiedź, a nie jedno i drugie. To nie pasuje do żądania API, ponieważ chcielibyśmy świeżych odpowiedzi za każdym razem, gdy żądanie jest wysyłane. Jednak oczekiwanie na nowe odpowiedzi opóźnia znaczącą użyteczność aplikacji.

Więc co robimy?

Wdrażamy niestandardowy mechanizm buforowania. W ramach tego wymyślamy sposób na zwrócenie zarówno pamięci podręcznej, jak i nowej odpowiedzi. W interfejsie użytkownika buforowana odpowiedź jest zastępowana nową odpowiedzią, gdy jest dostępna. Tak wyglądałaby logika:

  1. Gdy żądanie jest wysyłane do punktu końcowego serwera API po raz pierwszy, zbuforuj odpowiedź, a następnie ją zwróć.
  2. Następnym razem, gdy wystąpi to samo żądanie API, natychmiast użyj buforowanej odpowiedzi.
  3. Następnie wyślij żądanie asynchronicznie, aby pobrać nową odpowiedź. Po nadejściu odpowiedzi asynchronicznie propaguj zmiany w interfejsie użytkownika i aktualizuj pamięć podręczną.

Takie podejście umożliwia natychmiastowe aktualizacje interfejsu użytkownika — ponieważ każde żądanie interfejsu API jest buforowane — ale także ewentualną poprawność interfejsu użytkownika, ponieważ świeże dane odpowiedzi są wyświetlane, gdy tylko są dostępne.

W tym samouczku zobaczymy krok po kroku, jak to wdrożyć. Nazwiemy to podejście nieaktualnym podczas odświeżania, ponieważ interfejs użytkownika jest faktycznie odświeżany po otrzymaniu nowej odpowiedzi.

Przygotowania: API

Aby rozpocząć ten samouczek, najpierw będziemy potrzebować interfejsu API, z którego pobieramy dane. Na szczęście dostępnych jest mnóstwo pozorowanych usług API. W tym samouczku użyjemy reqres.in.

Pobierane przez nas dane to lista użytkowników z parametrem zapytania o page . Tak wygląda kod pobierania:

 fetch("https://reqres.in/api/users?page=2") .then(res => res.json()) .then(json => { console.log(json); });

Uruchomienie tego kodu daje nam następujące dane wyjściowe. Oto jego nie powtarzająca się wersja:

 { page: 2, per_page: 6, total: 12, total_pages: 2, data: [ { id: 7, email: "[email protected]", first_name: "Michael", last_name: "Lawson", avatar: "https://s3.amazonaws.com/uifaces/faces/twitter/follettkyle/128.jpg" }, // 5 more items ] }

Widać, że to jest jak prawdziwe API. W odpowiedzi mamy paginację. Parametr zapytania o page odpowiada za zmianę strony, a w zbiorze danych mamy w sumie dwie strony.

Korzystanie z API w aplikacji React

Zobaczmy, jak korzystamy z API w aplikacji React. Kiedy już wiemy, jak to zrobić, dowiemy się, jaka jest część buforowania. Będziemy używać klasy do tworzenia naszego komponentu. Oto kod:

 import React from "react"; import PropTypes from "prop-types"; export default class Component extends React.Component { state = { users: [] }; componentDidMount() { this.load(); } load() { fetch(`https://reqres.in/api/users?page=${this.props.page}`) .then(res => res.json()) .then(json => { this.setState({ users: json.data }); }); } componentDidUpdate(prevProps) { if (prevProps.page !== this.props.page) { this.load(); } } render() { const users = this.state.users.map(user => ( <p key={user.id}> <img src={user.avatar} alt={user.first_name} style={{ height: 24, width: 24 }} /> {user.first_name} {user.last_name} </p> )); return <div>{users}</div>; } } Component.propTypes = { page: PropTypes.number.isRequired };

Zauważ, że otrzymujemy wartość page poprzez props , jak to często bywa w rzeczywistych aplikacjach. Ponadto mamy funkcję componentDidUpdate , która ponownie pobiera dane API za każdym razem, gdy zmieni this.props.page .

W tym momencie pokazuje listę sześciu użytkowników, ponieważ interfejs API zwraca sześć pozycji na stronę:

Podgląd naszego prototypu komponentu React: sześć wyśrodkowanych linii, każda ze zdjęciem po lewej stronie nazwy.

Dodawanie pamięci podręcznej przeterminowania podczas odświeżania

Jeśli chcemy dodać do tego buforowanie nieaktualne podczas odświeżania, musimy zaktualizować logikę naszej aplikacji do:

  1. Buforuj jednoznacznie odpowiedź na żądanie po jego pierwszym pobraniu.
  2. Natychmiast zwróć zbuforowaną odpowiedź, jeśli zostanie znaleziona pamięć podręczna żądania. Następnie wyślij żądanie i asynchronicznie zwróć nową odpowiedź. Przechowuj również tę odpowiedź w pamięci podręcznej na następny raz.

Możemy to zrobić, mając globalny obiekt CACHE , który przechowuje pamięć podręczną w unikalny sposób. Dla wyjątkowości możemy użyć wartości this.props.page jako klucza w naszym obiekcie CACHE . Następnie po prostu kodujemy algorytm wspomniany powyżej.

 import apiFetch from "./apiFetch"; const CACHE = {}; export default class Component extends React.Component { state = { users: [] }; componentDidMount() { this.load(); } load() { if (CACHE[this.props.page] !== undefined) { this.setState({ users: CACHE[this.props.page] }); } apiFetch(`https://reqres.in/api/users?page=${this.props.page}`).then( json => { CACHE[this.props.page] = json.data; this.setState({ users: json.data }); } ); } componentDidUpdate(prevProps) { if (prevProps.page !== this.props.page) { this.load(); } } render() { // same render code as above } }

Ponieważ pamięć podręczna jest zwracana, gdy tylko zostanie znaleziona, a nowe dane odpowiedzi są również zwracane przez setState , oznacza to, że mamy płynne aktualizacje interfejsu użytkownika i nie ma już czasu oczekiwania na aplikację od drugiego żądania. To jest idealne i jest to w skrócie metoda nieświeża podczas odświeżania.

Schemat blokowy śledzący logikę nieaktualności podczas odświeżania. Zaczyna się od prośby. Jeśli jest buforowany, wywoływana jest setState() z buforowaną odpowiedzią. Tak czy inaczej, żądanie jest wysyłane, pamięć podręczna jest ustawiana, a setState() jest wywoływana z nową odpowiedzią.

Funkcja apiFetch jest tutaj niczym innym jak opakowaniem nad fetch , dzięki czemu możemy zobaczyć zalety buforowania w czasie rzeczywistym. Robi to poprzez dodanie losowego użytkownika do listy users zwróconych przez żądanie API. Dodaje do niego również losowe opóźnienie:

 export default async function apiFetch(...args) { await delay(Math.ceil(400 + Math.random() * 300)); const res = await fetch(...args); const json = await res.json(); json.data.push(getFakeUser()); return json; } function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }

Funkcja getFakeUser() jest odpowiedzialna za tworzenie fałszywego obiektu użytkownika.

Dzięki tym zmianom nasze API jest bardziej realne niż wcześniej.

  1. Ma losowe opóźnienie w odpowiedzi.
  2. Zwraca nieco inne dane dla tych samych żądań.

Biorąc to pod uwagę, gdy zmienimy właściwość page przekazaną do Component z naszego głównego komponentu, możemy zobaczyć buforowanie API w akcji. Spróbuj kliknąć przycisk Przełącz raz na kilka sekund w tym CodeSandbox i powinieneś zobaczyć takie zachowanie:

Animacja przedstawiająca przełączanie strony z włączonym buforowaniem. Szczegóły zostały opisane w artykule.

Jeśli przyjrzysz się uważnie, wydarzy się kilka rzeczy.

  1. Gdy aplikacja uruchamia się i jest w stanie domyślnym, widzimy listę siedmiu użytkowników. Zwróć uwagę na ostatniego użytkownika na liście, ponieważ jest to użytkownik, który zostanie losowo zmodyfikowany przy następnym wysłaniu tego żądania.
  2. Kiedy klikamy Przełącz po raz pierwszy, czeka przez krótki czas (400-700ms), a następnie aktualizuje listę do następnej strony.
  3. Teraz jesteśmy na drugiej stronie. Ponownie zanotuj ostatniego użytkownika na liście.
  4. Teraz ponownie klikamy Przełącz, a aplikacja powróci do pierwszej strony. Zauważ, że teraz ostatni wpis jest nadal tym samym użytkownikiem, który zanotowaliśmy w kroku 1, a później zmienia się na nowego (losowego) użytkownika. Dzieje się tak dlatego, że początkowo pamięć podręczna była pokazywana, a następnie pojawiła się właściwa odpowiedź.
  5. Ponownie klikamy Przełącz. Zdarza się to samo zjawisko. Buforowana odpowiedź z ostatniego razu jest ładowana natychmiast, a następnie pobierane są nowe dane, więc widzimy ostatnią aktualizację wpisu z tego, co zanotowaliśmy w kroku 3.

To jest to, buforowanie nieaktualne podczas odświeżania, którego szukaliśmy. Ale to podejście ma problem z powielaniem kodu. Zobaczmy, jak to działa, jeśli mamy inny komponent do pobierania danych z buforowaniem. Ten składnik pokazuje przedmioty inaczej niż nasz pierwszy składnik.

Dodawanie przestarzałych podczas odświeżania do innego komponentu

Możemy to zrobić, po prostu kopiując logikę z pierwszego komponentu. Nasz drugi komponent przedstawia listę kotów:

 const CACHE = {}; export default class Component2 extends React.Component { state = { cats: [] }; componentDidMount() { this.load(); } load() { if (CACHE[this.props.page] !== undefined) { this.setState({ cats: CACHE[this.props.page] }); } apiFetch(`https://reqres.in/api/cats?page=${this.props.page}`).then( json => { CACHE[this.props.page] = json.data; this.setState({ cats: json.data }); } ); } componentDidUpdate(prevProps) { if (prevProps.page !== this.props.page) { this.load(); } } render() { const cats = this.state.cats.map(cat => ( <p key={cat.id} style={{ background: cat.color, padding: "4px", width: 240 }} > {cat.name} (born {cat.year}) </p> )); return <div>{cats}</div>; } }

Jak widać, logika komponentów tutaj jest prawie taka sama, jak w przypadku pierwszego komponentu. Jedyna różnica dotyczy żądanego punktu końcowego i inaczej pokazuje elementy listy.

Teraz pokazujemy oba te komponenty obok siebie. Widać, że zachowują się podobnie:

Animacja przedstawiająca przełączanie z dwoma komponentami obok siebie.

Aby osiągnąć ten wynik, musieliśmy dużo powielać kodu. Gdybyśmy mieli wiele takich komponentów, duplikowalibyśmy zbyt dużo kodu.

Aby rozwiązać ten problem bez duplikowania, możemy mieć komponent wyższego rzędu do pobierania i buforowania danych oraz przekazywania ich jako właściwości. To nie jest idealne, ale zadziała. Ale gdybyśmy musieli wykonać wiele żądań w jednym komponencie, posiadanie wielu komponentów wyższego rzędu bardzo szybko stałoby się brzydkie.

Następnie mamy wzorzec render props, który jest prawdopodobnie najlepszym sposobem na zrobienie tego w komponentach klas. Działa doskonale, ale z drugiej strony jest podatny na „owijające piekło” i czasami wymaga od nas związania aktualnego kontekstu. To nie jest wspaniałe doświadczenie dla programistów i może prowadzić do frustracji i błędów.

To tutaj React Hooki ratują sytuację. Pozwalają nam umieścić logikę komponentów w pojemniku wielokrotnego użytku, dzięki czemu możemy z niej korzystać w wielu miejscach. Hooki Reacta zostały wprowadzone w React 16.8 i działają tylko z komponentami funkcyjnymi. Zanim przejdziemy do kontroli pamięci podręcznej React — w szczególności buforowania zawartości za pomocą hooków — zobaczmy najpierw, jak robimy proste pobieranie danych w komponentach funkcyjnych.

Pobieranie danych API w komponentach funkcyjnych

Aby pobrać dane API w komponentach funkcyjnych, używamy useState i useEffect .

useState jest analogiczne do state komponentów klasy i setState . Używamy tego haka, aby mieć atomowe kontenery stanu wewnątrz komponentu funkcji.

useEffect jest hakiem cyklu życia i można o nim myśleć jako o kombinacji componentDidMount , componentDidUpdate i componentWillUnmount . Drugi parametr przekazywany do useEffect nazywa się tablicą zależności. Gdy tablica zależności ulegnie zmianie, wywołanie zwrotne przekazane jako pierwszy argument do useEffect jest uruchamiane ponownie.

Oto jak wykorzystamy te haki do zaimplementowania pobierania danych:

 import React, { useState, useEffect } from "react"; export default function Component({ page }) { const [users, setUsers] = useState([]); useEffect(() => { fetch(`https://reqres.in/api/users?page=${page}`) .then(res => res.json()) .then(json => { setUsers(json.data); }); }, [page]); const usersDOM = users.map(user => ( <p key={user.id}> <img src={user.avatar} alt={user.first_name} style={{ height: 24, width: 24 }} /> {user.first_name} {user.last_name} </p> )); return <div>{usersDOM}</div>; }

Określając page jako zależność od useEffect , nakazujemy Reactowi uruchamianie naszego wywołania zwrotnego useEffect za każdym razem, gdy page jest zmieniana. To jest jak componentDidUpdate . Ponadto useEffect zawsze uruchamia się za pierwszym razem, więc działa również jak componentDidMount .

Nieaktualne odświeżanie w komponentach funkcyjnych

Wiemy, że useEffect jest podobny do metod cyklu życia komponentów. Możemy więc zmodyfikować przekazaną do niej funkcję wywołania zwrotnego, aby utworzyć buforowanie nieaktualne podczas odświeżania, które mieliśmy w komponentach klasy. Wszystko pozostaje bez zmian, z wyjątkiem haka useEffect .

 const CACHE = {}; export default function Component({ page }) { const [users, setUsers] = useState([]); useEffect(() => { if (CACHE[page] !== undefined) { setUsers(CACHE[page]); } apiFetch(`https://reqres.in/api/users?page=${page}`).then(json => { CACHE[page] = json.data; setUsers(json.data); }); }, [page]); // ... create usersDOM from users return <div>{usersDOM}</div>; }

W ten sposób mamy buforowanie nieaktualne podczas odświeżania działające w komponencie funkcyjnym.

Możemy zrobić to samo dla drugiego komponentu, to znaczy przekonwertować go do funkcji i zaimplementować buforowanie nieaktualne podczas odświeżania. Wynik będzie identyczny z tym, jaki mieliśmy na zajęciach.

Ale to wcale nie jest lepsze niż komponenty klasy, prawda? Zobaczmy więc, jak możemy wykorzystać moc niestandardowego hooka do stworzenia modułowej logiki nieaktualnej podczas odświeżania, której możemy użyć w wielu komponentach.

Niestandardowy hak do nieświeżego czasu podczas odświeżania

Najpierw zawęźmy logikę, którą chcemy przenieść do niestandardowego haka. Jeśli spojrzysz na poprzedni kod, wiesz, że to część useState i useEffect . Dokładniej, to jest logika, którą chcemy zmodularyzować.

 const [users, setUsers] = useState([]); useEffect(() => { if (CACHE[page] !== undefined) { setUsers(CACHE[page]); } apiFetch(`https://reqres.in/api/users?page=${page}`).then(json => { CACHE[page] = json.data; setUsers(json.data); }); }, [page]);

Ponieważ musimy uczynić go ogólnym, będziemy musieli uczynić adres URL dynamicznym. Więc musimy mieć url jako argument. Będziemy musieli również zaktualizować logikę buforowania, ponieważ wiele żądań może mieć tę samą wartość page . Na szczęście, gdy page jest dołączona do adresu URL punktu końcowego, daje unikalną wartość dla każdego unikalnego żądania. Możemy więc użyć całego adresu URL jako klucza do buforowania:

 const [data, setData] = useState([]); useEffect(() => { if (CACHE[url] !== undefined) { setData(CACHE[url]); } apiFetch(url).then(json => { CACHE[url] = json.data; setData(json.data); }); }, [url]);

To prawie wszystko. Po owinięciu go w funkcję otrzymamy nasz niestandardowy hook. Zajrzyj poniżej.

 const CACHE = {}; export default function useStaleRefresh(url, defaultValue = []) { const [data, setData] = useState(defaultValue); useEffect(() => { // cacheID is how a cache is identified against a unique request const cacheID = url; // look in cache and set response if present if (CACHE[cacheID] !== undefined) { setData(CACHE[cacheID]); } // fetch new data apiFetch(url).then(newData => { CACHE[cacheID] = newData.data; setData(newData.data); }); }, [url]); return data; }

Zauważ, że dodaliśmy do niego kolejny argument o nazwie defaultValue . Domyślna wartość wywołania API może być inna, jeśli użyjesz tego podpięcia w wielu komponentach. Dlatego sprawiliśmy, że można go dostosować.

To samo można zrobić dla klucza data w obiekcie newData . Jeśli niestandardowe podpięcie zwraca różne dane, możesz po prostu zwrócić newData , a nie newData.data i obsłużyć to przechodzenie po stronie komponentu.

Teraz, gdy mamy nasz niestandardowy hak, który wykonuje ciężkie operacje związane z buforowaniem nieaktualnych podczas odświeżania, oto jak podłączamy go do naszych komponentów. Zwróć uwagę na samą ilość kodu, który udało nam się zredukować. Cały nasz składnik to teraz tylko trzy stwierdzenia. To wielka wygrana.

 import useStaleRefresh from "./useStaleRefresh"; export default function Component({ page }) { const users = useStaleRefresh(`https://reqres.in/api/users?page=${page}`, []); const usersDOM = users.map(user => ( <p key={user.id}> <img src={user.avatar} alt={user.first_name} style={{ height: 24, width: 24 }} /> {user.first_name} {user.last_name} </p> )); return <div>{usersDOM}</div>; }

To samo możemy zrobić dla drugiego komponentu. Będzie to wyglądać tak:

 export default function Component2({ page }) { const cats = useStaleRefresh(`https://reqres.in/api/cats?page=${page}`, []); // ... create catsDOM from cats return <div>{catsDOM}</div>; }

Łatwo zobaczyć, ile kodu wzorcowego możemy zaoszczędzić, jeśli użyjemy tego haka. Kod też wygląda lepiej. Jeśli chcesz zobaczyć całą aplikację w akcji, przejdź do tej CodeSandbox.

Dodawanie wskaźnika ładowania do useStaleRefresh

Teraz, gdy mamy już podstawy, możemy dodać więcej funkcji do naszego niestandardowego haka. Na przykład możemy dodać wartość isLoading do podpięcia, która jest prawdziwa za każdym razem, gdy wysyłane jest unikalne żądanie, a w międzyczasie nie mamy żadnej pamięci podręcznej do pokazania.

Robimy to, mając osobny stan dla isLoading i ustawiając go zgodnie ze stanem podpięcia. Oznacza to, że gdy nie jest dostępna żadna zawartość sieci w pamięci podręcznej, ustawiamy ją na true , w przeciwnym razie ustawiamy ją na false .

Oto zaktualizowany hak:

 export default function useStaleRefresh(url, defaultValue = []) { const [data, setData] = useState(defaultValue); const [isLoading, setLoading] = useState(true); useEffect(() => { // cacheID is how a cache is identified against a unique request const cacheID = url; // look in cache and set response if present if (CACHE[cacheID] !== undefined) { setData(CACHE[cacheID]); setLoading(false); } else { // else make sure loading set to true setLoading(true); } // fetch new data apiFetch(url).then(newData => { CACHE[cacheID] = newData.data; setData(newData.data); setLoading(false); }); }, [url]); return [data, isLoading]; }

Możemy teraz używać nowej wartości isLoading w naszych komponentach.

 export default function Component({ page }) { const [users, isLoading] = useStaleRefresh( `https://reqres.in/api/users?page=${page}`, [] ); if (isLoading) { return <div>Loading</div>; } // ... create usersDOM from users return <div>{usersDOM}</div>; }

Zauważ, że po wykonaniu tej czynności zobaczysz tekst „Ładowanie”, gdy unikalne żądanie jest wysyłane po raz pierwszy i nie ma pamięci podręcznej.

Animacja przedstawiająca komponent z zaimplementowanym wskaźnikiem ładowania.

Korzystanie ze wsparcia useStaleRefresh Dowolna funkcja async

Możemy sprawić, że nasz niestandardowy hook będzie jeszcze potężniejszy, dzięki czemu obsługuje dowolną funkcję async , a nie tylko żądania sieciowe GET . Podstawowa idea, która się za tym kryje, pozostanie taka sama.

  1. W haczyku wywołujesz funkcję asynchroniczną, która po pewnym czasie zwraca wartość.
  2. Każde unikalne wywołanie funkcji asynchronicznej jest prawidłowo buforowane.

Proste połączenie nazwy function.name i arguments będzie działać jako klucz pamięci podręcznej dla naszego przypadku użycia. Używając tego, nasz haczyk będzie wyglądał tak:

 import { useState, useEffect, useRef } from "react"; import isEqual from "lodash/isEqual"; const CACHE = {}; export default function useStaleRefresh(fn, args, defaultValue = []) { const prevArgs = useRef(null); const [data, setData] = useState(defaultValue); const [isLoading, setLoading] = useState(true); useEffect(() => { // args is an object so deep compare to rule out false changes if (isEqual(args, prevArgs.current)) { return; } // cacheID is how a cache is identified against a unique request const cacheID = hashArgs(fn.name, ...args); // look in cache and set response if present if (CACHE[cacheID] !== undefined) { setData(CACHE[cacheID]); setLoading(false); } else { // else make sure loading set to true setLoading(true); } // fetch new data fn(...args).then(newData => { CACHE[cacheID] = newData; setData(newData); setLoading(false); }); }, [args, fn]); useEffect(() => { prevArgs.current = args; }); return [data, isLoading]; } function hashArgs(...args) { return args.reduce((acc, arg) => stringify(arg) + ":" + acc, ""); } function stringify(val) { return typeof val === "object" ? JSON.stringify(val) : String(val); }

Jak widać, używamy kombinacji nazwy funkcji i jej sprecyzowanych argumentów, aby jednoznacznie zidentyfikować wywołanie funkcji, a tym samym je buforować. Działa to w przypadku naszej prostej aplikacji, ale ten algorytm jest podatny na kolizje i powolne porównania. (W przypadku argumentów, których nie można serializować, w ogóle nie będzie działać). Tak więc w przypadku aplikacji w świecie rzeczywistym odpowiedni algorytm mieszający jest bardziej odpowiedni.

Kolejną rzeczą, na którą warto zwrócić uwagę, jest użycie useRef . useRef służy do utrwalania danych przez cały cykl życia otaczającego składnika. Ponieważ args jest tablicą — która jest obiektem w JavaScript — każde ponowne renderowanie komponentu przy użyciu hooka powoduje zmianę wskaźnika odwołania do args . Ale args jest częścią listy zależności w naszym pierwszym useEffect . Tak więc zmiana args może sprawić, że nasz useEffect działał nawet wtedy, gdy nic się nie zmieni. Aby temu przeciwdziałać, dokonujemy głębokiego porównania starych i aktualnych args za pomocą args i zezwalamy na uruchomienie wywołania zwrotnego useEffect tylko wtedy, gdy rzeczywiście uległy one zmianie.

Teraz możemy użyć tego nowego haka useStaleRefresh w następujący sposób. Zwróć uwagę na zmianę w defaultValue tutaj. Ponieważ jest to hak ogólnego przeznaczenia, nie polegamy na tym, że nasz hak zwróci klucz data w obiekcie odpowiedzi.

 export default function Component({ page }) { const [users, isLoading] = useStaleRefresh( apiFetch, [`https://reqres.in/api/users?page=${page}`], { data: [] } ); if (isLoading) { return <div>Loading</div>; } const usersDOM = users.data.map(user => ( <p key={user.id}> <img src={user.avatar} alt={user.first_name} style={{ height: 24, width: 24 }} /> {user.first_name} {user.last_name} </p> )); return <div>{usersDOM}</div>; }

Cały kod można znaleźć w tej CodeSandbox.

Nie każ użytkownikom czekać: efektywnie wykorzystuj zawartość pamięci podręcznej z hakami przestarzałymi podczas odświeżania i reakcją

useStaleRefresh , który stworzyliśmy w tym artykule, jest dowodem koncepcji, który pokazuje, co jest możliwe dzięki hakom React. Spróbuj pobawić się kodem i sprawdź, czy możesz go zmieścić w swojej aplikacji.

Alternatywnie możesz również spróbować wykorzystać nieaktualne podczas odświeżania za pośrednictwem popularnej, dobrze utrzymanej biblioteki open-source, takiej jak swr lub React-query. Obie są potężnymi bibliotekami i obsługują wiele funkcji, które pomagają w żądaniach API.

React Hooki zmieniają zasady gry. Pozwalają nam elegancko udostępniać logikę komponentów. Wcześniej nie było to możliwe, ponieważ stan komponentu, metody cyklu życia i renderowanie zostały umieszczone w jednej encji: komponenty klasy. Teraz możemy mieć różne moduły dla nich wszystkich. To świetnie nadaje się do komponowania i pisania lepszego kodu. Używam komponentów funkcyjnych i hooków do całego nowego kodu Reacta, który piszę i gorąco polecam to wszystkim programistom React.

Powiązane: Tworzenie aplikacji React za pomocą zestawu narzędzi Redux i zapytania RTK