Jak utworzyć niestandardowy hak reakcji w celu pobrania i przechowywania danych w pamięci podręcznej?

Opublikowany: 2022-03-10
Szybkie podsumowanie ↬ Istnieje duże prawdopodobieństwo, że wiele komponentów w Twojej aplikacji React będzie musiało wykonywać wywołania API w celu pobrania danych, które będą wyświetlane użytkownikom. Jest to już możliwe za pomocą metody cyklu życia componentDidMount() , ale wraz z wprowadzeniem hooków możesz zbudować własny hook, który będzie pobierał i buforował dane dla ciebie. To właśnie omówi ten samouczek.

Jeśli jesteś nowicjuszem w React Hooks, możesz zacząć od sprawdzenia oficjalnej dokumentacji, aby to zrozumieć. Następnie polecam przeczytanie „Getting Started With React Hooks API” Shedracka Akintayo. Aby upewnić się, że podążasz dalej, istnieje również artykuł napisany przez Adeneye David Abiodun, który omawia najlepsze praktyki z hakami React, które z pewnością okażą się dla Ciebie przydatne.

W tym artykule będziemy korzystać z interfejsu Hacker News Search API do zbudowania niestandardowego haka, którego będziemy mogli użyć do pobierania danych. Chociaż w tym samouczku omówimy interfejs API Hacker News Search, hak będzie działał w taki sposób, że zwróci odpowiedź z dowolnego prawidłowego łącza API, które do niego przekażemy.

Najlepsze praktyki reagowania

React to fantastyczna biblioteka JavaScript do budowania bogatych interfejsów użytkownika. Zapewnia świetną abstrakcję komponentów do organizowania interfejsów w dobrze działający kod i jest prawie wszystko, do czego możesz go użyć. Przeczytaj powiązany artykuł na temat React →

Pobieranie danych w komponencie React

Przed przechwyceniem Reacta zwyczajowo pobierano dane początkowe w metodzie cyklu życia componentDidMount() oraz dane na podstawie zmian właściwości lub stanu w metodzie cyklu życia componentDidUpdate() .

Oto jak to działa:

 componentDidMount() { const fetchData = async () => { const response = await fetch( `https://hn.algolia.com/api/v1/search?query=JavaScript` ); const data = await response.json(); this.setState({ data }); }; fetchData(); } componentDidUpdate(previousProps, previousState) { if (previousState.query !== this.state.query) { const fetchData = async () => { const response = await fetch( `https://hn.algolia.com/api/v1/search?query=${this.state.query}` ); const data = await response.json(); this.setState({ data }); }; fetchData(); } }

Metoda cyklu życia componentDidMount jest wywoływana, gdy tylko komponent zostanie zamontowany, a kiedy to zostanie zrobione, zrobiliśmy żądanie wyszukania „JavaScript” za pośrednictwem interfejsu Hacker News API i zaktualizowania stanu na podstawie odpowiedzi.

Z drugiej strony metoda cyklu życia componentDidUpdate jest wywoływana, gdy nastąpi zmiana w składniku. Porównaliśmy poprzednie zapytanie w stanie z bieżącym zapytaniem, aby zapobiec wywoływaniu metody za każdym razem, gdy ustawiamy „data” w stanie. Jedną z rzeczy, które uzyskujemy dzięki używaniu hooków, jest połączenie obu metod cyklu życia w czystszy sposób — co oznacza, że ​​nie będziemy potrzebować dwóch metod cyklu życia dla montowania i aktualizacji komponentu.

Więcej po skoku! Kontynuuj czytanie poniżej ↓

Pobieranie danych za pomocą useEffect

Hak useEffect jest wywoływany zaraz po zamontowaniu komponentu. Jeśli potrzebujemy, aby hak został uruchomiony ponownie na podstawie niektórych zmian właściwości lub stanu, musimy przekazać je do tablicy zależności (która jest drugim argumentem haka useEffect ).

Przyjrzyjmy się, jak pobierać dane za pomocą hooków:

 import { useState, useEffect } from 'react'; const [status, setStatus] = useState('idle'); const [query, setQuery] = useState(''); const [data, setData] = useState([]); useEffect(() => { if (!query) return; const fetchData = async () => { setStatus('fetching'); const response = await fetch( `https://hn.algolia.com/api/v1/search?query=${query}` ); const data = await response.json(); setData(data.hits); setStatus('fetched'); }; fetchData(); }, [query]);

W powyższym przykładzie przekazaliśmy query jako zależność do naszego useEffect . Robiąc to, mówimy useEffect , aby śledził zmiany w zapytaniach. Jeśli poprzednia wartość query nie jest taka sama jak wartość bieżąca, useEffect zostanie wywołany ponownie.

Mając to na uwadze, w razie potrzeby ustawiamy również kilka status komponentu, ponieważ będzie to lepiej przekazywać komunikaty na ekranie w oparciu o stan niektórych status skończonych . W stanie bezczynności możemy poinformować użytkowników, że mogą skorzystać z pola wyszukiwania, aby rozpocząć. W stanie pobierania moglibyśmy pokazać spinner . A w stanie pobranym wyrenderujemy dane.

Ważne jest, aby ustawić dane przed próbą ustawienia statusu na fetched , aby zapobiec migotaniu, które występuje w wyniku braku danych podczas ustawiania statusu fetched .

Tworzenie niestandardowego hooka

„Niestandardowy hook to funkcja JavaScript, której nazwa zaczyna się od 'use' i która może wywoływać inne hooki.”

— React Docs

Tak właśnie jest, a wraz z funkcją JavaScript umożliwia ponowne użycie fragmentu kodu w kilku częściach aplikacji.

Definicja z React Docs zdradziła to, ale zobaczmy, jak działa w praktyce z niestandardowym hakiem licznika:

 const useCounter = (initialState = 0) => { const [count, setCount] = useState(initialState); const add = () => setCount(count + 1); const subtract = () => setCount(count - 1); return { count, add, subtract }; };

Tutaj mamy zwykłą funkcję, w której pobieramy opcjonalny argument, ustawiamy wartość na nasz stan, a także dodajemy metody add i subtract , których można użyć do jej aktualizacji.

Wszędzie w naszej aplikacji, gdzie potrzebujemy licznika, możemy wywołać useCounter jak zwykłą funkcję i przekazać initialState , abyśmy wiedzieli, od czego zacząć liczenie. Gdy nie mamy stanu początkowego, domyślnie przyjmujemy 0.

Oto jak to działa w praktyce:

 import { useCounter } from './customHookPath'; const { count, add, subtract } = useCounter(100); eventHandler(() => { add(); // or subtract(); });

To, co tutaj zrobiliśmy, to zaimportowanie naszego niestandardowego haka z pliku, w którym go zadeklarowaliśmy, abyśmy mogli go wykorzystać w naszej aplikacji. Ustawiamy jego stan początkowy na 100, więc za każdym razem, gdy wywołujemy add() , zwiększa count o 1, a za każdym razem, gdy wywołujemy subtract() , zmniejsza count o 1.

Tworzenie useFetch Hook

Teraz, gdy nauczyliśmy się tworzyć prosty niestandardowy zaczep, wyodrębnijmy naszą logikę, aby pobrać dane do niestandardowego zaczepu.

 const useFetch = (query) => { const [status, setStatus] = useState('idle'); const [data, setData] = useState([]); useEffect(() => { if (!query) return; const fetchData = async () => { setStatus('fetching'); const response = await fetch( `https://hn.algolia.com/api/v1/search?query=${query}` ); const data = await response.json(); setData(data.hits); setStatus('fetched'); }; fetchData(); }, [query]); return { status, data }; };

To prawie to samo, co zrobiliśmy powyżej, z wyjątkiem tego, że jest to funkcja, która pobiera query i zwraca status oraz data . I to jest hak useFetch , którego moglibyśmy użyć w kilku komponentach w naszej aplikacji React.

To działa, ale problem z tą implementacją jest teraz specyficzny dla Hacker News, więc możemy nazwać to po prostu useHackerNews . To, co zamierzamy zrobić, to stworzyć hak useFetch , który może być użyty do wywołania dowolnego adresu URL. Zmieńmy to, aby zamiast tego wziąć adres URL!

 const useFetch = (url) => { const [status, setStatus] = useState('idle'); const [data, setData] = useState([]); useEffect(() => { if (!url) return; const fetchData = async () => { setStatus('fetching'); const response = await fetch(url); const data = await response.json(); setData(data); setStatus('fetched'); }; fetchData(); }, [url]); return { status, data }; };

Teraz nasz haczyk useFetch jest ogólny i możemy go używać w różnych komponentach.

Oto jeden sposób na jego spożycie:

 const [query, setQuery] = useState(''); const url = query && `https://hn.algolia.com/api/v1/search?query=${query}`; const { status, data } = useFetch(url);

W takim przypadku, jeśli wartość query jest truthy , kontynuujemy ustawianie adresu URL, a jeśli tak nie jest, możemy przekazać undefined, ponieważ zostanie to obsłużone w naszym przechwyceniu. Niezależnie od tego efekt spróbuje uruchomić się raz.

Zapamiętywanie pobranych danych

Zapamiętywanie to technika, której użyjemy, aby upewnić się, że nie trafimy do punktu końcowego hackernews , jeśli złożymy jakąś prośbę o pobranie go w jakiejś początkowej fazie. Przechowywanie wyników kosztownych wywołań pobierania zaoszczędzi użytkownikom trochę czasu wczytywania, co zwiększy ogólną wydajność.

Uwaga : Aby uzyskać więcej kontekstu, możesz zapoznać się z wyjaśnieniem Wikipedii na temat zapamiętywania.

Zobaczmy, jak możemy to zrobić!

 const cache = {}; const useFetch = (url) => { const [status, setStatus] = useState('idle'); const [data, setData] = useState([]); useEffect(() => { if (!url) return; const fetchData = async () => { setStatus('fetching'); if (cache[url]) { const data = cache[url]; setData(data); setStatus('fetched'); } else { const response = await fetch(url); const data = await response.json(); cache[url] = data; // set response in cache; setData(data); setStatus('fetched'); } }; fetchData(); }, [url]); return { status, data }; };

Tutaj mapujemy adresy URL do ich danych. Tak więc, jeśli wysyłamy żądanie pobrania istniejących danych, ustawiamy dane z naszej lokalnej pamięci podręcznej, w przeciwnym razie wykonujemy żądanie i ustawiamy wynik w pamięci podręcznej. Gwarantuje to, że nie wykonujemy wywołania API, gdy mamy dane dostępne lokalnie. Zauważymy też, że usuwamy efekt, jeśli adres URL jest falsy , dzięki czemu nie będziemy kontynuować pobierania danych, które nie istnieją. Nie możemy tego zrobić przed hakiem useEffect , ponieważ jest to sprzeczne z jedną z zasad haczyków, która polega na tym, aby zawsze wywoływać haki na najwyższym poziomie.

Zadeklarowanie cache w innym zakresie działa, ale powoduje, że nasz hook jest sprzeczny z zasadą czystej funkcji. Poza tym chcemy również upewnić się, że React pomaga w posprzątaniu naszego bałaganu, gdy nie chcemy już korzystać z komponentu. Zbadamy useRef , aby pomóc nam w osiągnięciu tego celu.

Zapamiętywanie danych z useRef

useRef jest jak pudełko, które może przechowywać zmienną wartość we .current property ”.

— React Docs

Dzięki useRef możemy łatwo ustawiać i pobierać zmienne wartości, a ich wartość jest utrzymywana przez cały cykl życia komponentu.

Zamieńmy naszą implementację pamięci podręcznej na trochę magii useRef !

 const useFetch = (url) => { const cache = useRef({}); const [status, setStatus] = useState('idle'); const [data, setData] = useState([]); useEffect(() => { if (!url) return; const fetchData = async () => { setStatus('fetching'); if (cache.current[url]) { const data = cache.current[url]; setData(data); setStatus('fetched'); } else { const response = await fetch(url); const data = await response.json(); cache.current[url] = data; // set response in cache; setData(data); setStatus('fetched'); } }; fetchData(); }, [url]); return { status, data }; };

Tutaj nasza pamięć podręczna jest teraz w naszym haczyku useFetch z pustym obiektem jako wartością początkową.

Zawijanie

Cóż, stwierdziłem, że ustawienie danych przed ustawieniem statusu pobranego było dobrym pomysłem, ale są też dwa potencjalne problemy, które możemy mieć z tym:

  1. Nasz test jednostkowy może się nie powieść, ponieważ tablica danych nie jest pusta, gdy jesteśmy w stanie pobierania. React może faktycznie zmieniać stany wsadowe, ale nie może tego zrobić, jeśli jest wyzwalany asynchronicznie;
  2. Nasza aplikacja ponownie renderuje więcej niż powinna.

Zróbmy ostatnie porządki w naszym haczyku useFetch . Zaczniemy od przełączenia naszych useState s na useReducer . Zobaczmy, jak to działa!

 const initialState = { status: 'idle', error: null, data: [], }; const [state, dispatch] = useReducer((state, action) => { switch (action.type) { case 'FETCHING': return { ...initialState, status: 'fetching' }; case 'FETCHED': return { ...initialState, status: 'fetched', data: action.payload }; case 'FETCH_ERROR': return { ...initialState, status: 'error', error: action.payload }; default: return state; } }, initialState);

Tutaj dodaliśmy stan początkowy, który jest wartością początkową, którą przekazaliśmy do każdego z naszych indywidualnych useState . W naszym useReducer sprawdzamy, jaki rodzaj akcji chcemy wykonać i na tej podstawie ustawiamy odpowiednie wartości do stanu.

Rozwiązuje to dwa problemy, które omówiliśmy wcześniej, ponieważ teraz możemy jednocześnie ustawić stan i dane, aby zapobiec niemożliwym stanom i niepotrzebnemu ponownemu renderowaniu.

Została jeszcze tylko jedna rzecz: oczyszczenie naszego efektu ubocznego. Fetch implementuje interfejs Promise API w tym sensie, że może zostać rozwiązany lub odrzucony. Jeśli nasz hook spróbuje dokonać aktualizacji, gdy komponent został odmontowany, ponieważ jakaś Promise została właśnie rozwiązana, React zwróci Can't perform a React state update on an unmounted component.

Zobaczmy, jak możemy to naprawić za pomocą czyszczenia useEffect !

 useEffect(() => { let cancelRequest = false; if (!url) return; const fetchData = async () => { dispatch({ type: 'FETCHING' }); if (cache.current[url]) { const data = cache.current[url]; dispatch({ type: 'FETCHED', payload: data }); } else { try { const response = await fetch(url); const data = await response.json(); cache.current[url] = data; if (cancelRequest) return; dispatch({ type: 'FETCHED', payload: data }); } catch (error) { if (cancelRequest) return; dispatch({ type: 'FETCH_ERROR', payload: error.message }); } } }; fetchData(); return function cleanup() { cancelRequest = true; }; }, [url]);

Tutaj ustawiamy cancelRequest na true po zdefiniowaniu go w efekcie. Tak więc, zanim spróbujemy dokonać zmian stanu, najpierw potwierdzamy, czy komponent został odmontowany. Jeśli został odmontowany, pomijamy aktualizację stanu, a jeśli nie był odmontowany, aktualizujemy stan. To rozwiąże błąd aktualizacji stanu React , a także zapobiegnie sytuacji wyścigu w naszych komponentach.

Wniosek

Zbadaliśmy kilka koncepcji zaczepów, aby pomóc w pobieraniu i buforowaniu danych w naszych komponentach. Przeszliśmy również przez wyczyszczenie naszego haczyka useEffect , który pomaga zapobiegać wielu problemom w naszej aplikacji.

Jeśli masz jakieś pytania, możesz je umieścić w sekcji komentarzy poniżej!

  • Zobacz repozytorium tego artykułu →

Bibliografia

  • „Przedstawiamy hooki”, React Docs
  • „Pierwsze kroki z interfejsem React Hooks API”, Shedrack Akintayo
  • „Najlepsze praktyki z hakami React”, Adeneye David Abiodun
  • „Programowanie funkcjonalne: czyste funkcje”, Arne Brasseur