So erstellen Sie einen benutzerdefinierten Reaktionshaken zum Abrufen und Zwischenspeichern von Daten

Veröffentlicht: 2022-03-10
Kurze Zusammenfassung ↬ Es besteht eine hohe Wahrscheinlichkeit, dass viele Komponenten in Ihrer React-Anwendung eine API aufrufen müssen, um Daten abzurufen, die Ihren Benutzern angezeigt werden. Dies ist bereits mit der Lebenszyklusmethode „ componentDidMount() “ möglich, aber mit der Einführung von Hooks können Sie einen benutzerdefinierten Hook erstellen, der die Daten für Sie abruft und zwischenspeichert. Darum geht es in diesem Tutorial.

Wenn Sie ein Neuling bei React Hooks sind, können Sie damit beginnen, die offizielle Dokumentation zu lesen, um sich einen Überblick zu verschaffen. Danach würde ich empfehlen, Shedrack Akintayos „Getting Started With React Hooks API“ zu lesen. Um sicherzustellen, dass Sie uns folgen, gibt es auch einen Artikel von Adeneye David Abiodun, der Best Practices mit React Hooks behandelt, von denen ich sicher bin, dass sie sich für Sie als nützlich erweisen werden.

In diesem Artikel verwenden wir die Hacker News Search API, um einen benutzerdefinierten Hook zu erstellen, mit dem wir Daten abrufen können. Während dieses Tutorial die Hacker News Search-API abdeckt, lassen wir den Hook so funktionieren, dass er eine Antwort von jedem gültigen API-Link zurückgibt, den wir an ihn übergeben.

Beste Reaktionspraktiken

React ist eine fantastische JavaScript-Bibliothek zum Erstellen umfangreicher Benutzeroberflächen. Es bietet eine großartige Komponentenabstraktion, um Ihre Schnittstellen in gut funktionierendem Code zu organisieren, und es gibt so ziemlich alles, wofür Sie es verwenden können. Lesen Sie einen verwandten Artikel auf React →

Abrufen von Daten in einer React-Komponente

Vor React-Hooks war es üblich, Anfangsdaten in der Lebenszyklusmethode „ componentDidMount() “ und Daten auf der Grundlage von Eigenschaften oder Zustandsänderungen in der Lebenszyklusmethode „ componentDidUpdate() “ abzurufen.

So funktioniert das:

 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(); } }

Die Lebenszyklusmethode „ componentDidMount “ wird aufgerufen, sobald die Komponente gemountet wird, und wenn das erledigt ist, haben wir eine Anfrage gestellt, um über die Hacker News-API nach „JavaScript“ zu suchen und den Status basierend auf der Antwort zu aktualisieren.

Die Lebenszyklusmethode „ componentDidUpdate “ hingegen wird aufgerufen, wenn eine Änderung an der Komponente auftritt. Wir haben die vorherige Abfrage im Status mit der aktuellen Abfrage verglichen, um zu verhindern, dass die Methode jedes Mal aufgerufen wird, wenn wir „Daten“ im Status festlegen. Eine Sache, die wir durch die Verwendung von Hooks erreichen, ist, dass wir beide Lebenszyklusmethoden sauberer kombinieren – was bedeutet, dass wir nicht zwei Lebenszyklusmethoden haben müssen, wenn die Komponente bereitgestellt wird und wann sie aktualisiert wird.

Mehr nach dem Sprung! Lesen Sie unten weiter ↓

Abrufen von Daten mit useEffect Hook

Der Hook useEffect wird aufgerufen, sobald die Komponente gemountet wird. Wenn wir den Hook basierend auf einigen Prop- oder Statusänderungen erneut ausführen müssen, müssen wir sie an das Abhängigkeitsarray übergeben (das das zweite Argument des useEffect ist).

Lassen Sie uns untersuchen, wie Daten mit Hooks abgerufen werden:

 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]);

Im obigen Beispiel haben wir query als Abhängigkeit an unseren useEffect Hook übergeben. Dadurch weisen wir useEffect an, useEffect zu verfolgen. Wenn der vorherige query nicht mit dem aktuellen Wert übereinstimmt, wird useEffect erneut aufgerufen.

Vor diesem Hintergrund setzen wir bei Bedarf auch mehrere status für die Komponente, da dies basierend auf einigen status endlicher Zustände besser eine Nachricht an den Bildschirm übermittelt. Im Ruhezustand könnten wir den Benutzern mitteilen, dass sie das Suchfeld verwenden können, um loszulegen. Im abholenden Zustand könnten wir einen Spinner zeigen. Und im abgerufenen Zustand rendern wir die Daten.

Es ist wichtig, die Daten festzulegen, bevor Sie versuchen, den Status auf „ fetched “ zu setzen, damit Sie ein Flimmern vermeiden können, das auftritt, wenn die Daten leer sind, während Sie den Status „ fetched “ festlegen.

Erstellen eines benutzerdefinierten Hooks

„Ein benutzerdefinierter Hook ist eine JavaScript-Funktion, deren Name mit ‚use‘ beginnt und die andere Hooks aufrufen kann.“

— Reagieren Sie auf Dokumente

Das ist es wirklich, und zusammen mit einer JavaScript-Funktion ermöglicht es Ihnen, einen Teil des Codes in mehreren Teilen Ihrer App wiederzuverwenden.

Die Definition aus den React Docs hat es verraten, aber mal sehen, wie es in der Praxis mit einem benutzerdefinierten Counter-Hook funktioniert:

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

Hier haben wir eine reguläre Funktion, bei der wir ein optionales Argument aufnehmen, den Wert auf unseren Status setzen und die add und subtract hinzufügen, die zum Aktualisieren verwendet werden könnten.

Überall in unserer App, wo wir einen Zähler benötigen, können wir useCounter wie eine normale Funktion aufrufen und einen initialState , damit wir wissen, wo wir mit dem Zählen beginnen müssen. Wenn wir keinen Anfangszustand haben, verwenden wir standardmäßig 0.

So funktioniert es in der Praxis:

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

Was wir hier getan haben, war, unseren benutzerdefinierten Hook aus der Datei zu importieren, in der wir ihn deklariert haben, damit wir ihn in unserer App verwenden können. Wir setzen seinen Anfangszustand auf 100. Wenn wir also add() aufrufen, erhöht es count um 1, und jedes Mal, wenn wir subtract() aufrufen, verringert es count um 1.

useFetch Hook erstellen

Nachdem wir nun gelernt haben, wie man einen einfachen benutzerdefinierten Hook erstellt, extrahieren wir unsere Logik zum Abrufen von Daten in einen benutzerdefinierten Hook.

 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 }; };

Es ist ziemlich dasselbe, was wir oben gemacht haben, mit der Ausnahme, dass es sich um eine Funktion handelt, die query aufnimmt und status und data zurückgibt. Und das ist ein useFetch -Hook, den wir in mehreren Komponenten unserer React-Anwendung verwenden könnten.

Das funktioniert, aber das Problem mit dieser Implementierung ist jetzt, dass sie spezifisch für Hacker News ist, also könnten wir sie einfach useHackerNews nennen. Was wir beabsichtigen, ist, einen useFetch Hook zu erstellen, der zum Aufrufen einer beliebigen URL verwendet werden kann. Lassen Sie uns es umgestalten, um stattdessen eine URL aufzunehmen!

 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 }; };

Jetzt ist unser useFetch-Hook generisch und wir können ihn nach Belieben in unseren verschiedenen Komponenten verwenden.

Hier ist eine Möglichkeit, es zu konsumieren:

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

Wenn in diesem Fall der Wert von query truthy ist, legen wir die URL fest, und wenn dies nicht der Fall ist, können wir undefined übergeben, da dies in unserem Hook behandelt würde. Der Effekt versucht trotzdem einmal zu laufen.

Abgeholte Daten merken

Memoization ist eine Technik, die wir verwenden würden, um sicherzustellen, dass wir den hackernews Endpunkt nicht erreichen, wenn wir in irgendeiner Anfangsphase irgendeine Anfrage gestellt haben, um ihn abzurufen. Das Speichern des Ergebnisses teurer Abrufaufrufe spart den Benutzern etwas Ladezeit und erhöht daher die Gesamtleistung.

Hinweis : Für mehr Kontext können Sie die Wikipedia-Erklärung zur Memoisierung lesen.

Lassen Sie uns herausfinden, wie wir das tun könnten!

 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 }; };

Hier ordnen wir URLs ihren Daten zu. Wenn wir also eine Anfrage stellen, um einige vorhandene Daten abzurufen, setzen wir die Daten aus unserem lokalen Cache, andernfalls stellen wir die Anfrage und legen das Ergebnis im Cache fest. Dadurch wird sichergestellt, dass wir keinen API-Aufruf tätigen, wenn uns die Daten lokal zur Verfügung stehen. Wir werden auch feststellen, dass wir den Effekt beenden, wenn die URL falsy ist, sodass sichergestellt wird, dass wir nicht mit dem Abrufen von Daten fortfahren, die nicht vorhanden sind. Wir können dies nicht vor dem useEffect Hook tun, da dies gegen eine der Hook-Regeln verstößt, nämlich Hooks immer auf der obersten Ebene aufzurufen.

Das Deklarieren von cache in einem anderen Bereich funktioniert, aber es verstößt gegen das Prinzip einer reinen Funktion. Außerdem möchten wir sicherstellen, dass React beim Aufräumen hilft, wenn wir die Komponente nicht mehr verwenden möchten. Wir werden useRef , um uns dabei zu helfen, dies zu erreichen.

Merken von Daten mit useRef

useRef ist wie eine Box, die einen veränderlichen Wert in ihrer .current property kann.“

— Reagieren Sie auf Dokumente

Mit useRef können wir problemlos veränderliche Werte festlegen und abrufen, und ihr Wert bleibt während des gesamten Lebenszyklus der Komponente erhalten.

Lassen Sie uns unsere Cache-Implementierung durch etwas useRef Magie ersetzen!

 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 }; };

Hier befindet sich unser Cache jetzt in unserem useFetch Hook mit einem leeren Objekt als Anfangswert.

Einpacken

Nun, ich habe gesagt, dass das Festlegen der Daten vor dem Festlegen des abgerufenen Status eine gute Idee war, aber es gibt auch zwei potenzielle Probleme, die wir damit haben könnten:

  1. Unser Komponententest könnte fehlschlagen, weil das Datenarray nicht leer ist, während wir uns im Abrufzustand befinden. React könnte tatsächlich Zustandsänderungen stapeln, aber es kann das nicht, wenn es asynchron ausgelöst wird;
  2. Unsere App rendert mehr als sie sollte.

Lassen Sie uns unseren useFetch -Hook abschließend aufräumen. Wir beginnen damit, dass wir unsere useState s in einen useReducer . Mal sehen, wie das funktioniert!

 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);

Hier haben wir einen Anfangszustand hinzugefügt, der der Anfangswert ist, den wir an jeden unserer individuellen useState s übergeben haben. In unserem useReducer prüfen wir, welche Art von Aktion wir ausführen möchten, und setzen basierend darauf die entsprechenden Werte auf state.

Dies löst die beiden Probleme, die wir zuvor besprochen haben, da wir jetzt den Status und die Daten gleichzeitig festlegen können, um unmögliche Zustände und unnötige Neuberechnungen zu verhindern.

Bleibt nur noch eines übrig: unseren Nebeneffekt zu beseitigen. Fetch implementiert die Promise-API in dem Sinne, dass sie aufgelöst oder abgelehnt werden könnte. Wenn unser Hook versucht, ein Update durchzuführen, während die Komponente aufgrund eines gerade gelösten Promise ausgehängt wurde, würde React Can't perform a React state update on an unmounted component.

Mal sehen, wie wir das mit der Bereinigung von useEffect beheben können!

 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]);

Hier setzen wir cancelRequest auf true , nachdem wir es innerhalb des Effekts definiert haben. Bevor wir also versuchen, Statusänderungen vorzunehmen, bestätigen wir zunächst, ob die Komponente ausgehängt wurde. Wenn es ausgehängt wurde, überspringen wir die Statusaktualisierung, und wenn es nicht ausgehängt wurde, aktualisieren wir den Status. Dadurch wird der React-Statusaktualisierungsfehler behoben und es werden auch Race-Bedingungen in unseren Komponenten verhindert.

Fazit

Wir haben mehrere Hook-Konzepte untersucht, um das Abrufen und Zwischenspeichern von Daten in unseren Komponenten zu unterstützen. Wir haben auch unseren useEffect Hook aufgeräumt, der dazu beiträgt, eine ganze Reihe von Problemen in unserer App zu vermeiden.

Wenn Sie Fragen haben, können Sie diese gerne im Kommentarbereich unten hinterlassen!

  • Siehe das Repo für diesen Artikel →

Verweise

  • „Einführung in Hooks“, React Docs
  • „Erste Schritte mit der React Hooks API“, Shedrack Akintayo
  • „Best Practices mit React Hooks“, Adeneye David Abiodun
  • „Funktionale Programmierung: Reine Funktionen“, Arne Brasseur