
Betrachten wir die Implementierung der Datenanforderung an die API mithilfe der React Hooks des neuen Freundes und der guten alten Freunde Render Prop und HOC (Component höherer Ordnung). Finden Sie heraus, ob ein neuer Freund wirklich besser ist als die beiden alten.
Das Leben steht nicht still, React verändert sich zum Besseren. Im Februar 2019 erschien React Hooks in React 16.8.0. Jetzt können Sie in den Funktionskomponenten mit dem lokalen Status arbeiten und Nebenwirkungen ausführen. Niemand glaubte, dass es möglich war, aber jeder wollte es immer. Wenn Sie mit Details nicht auf dem neuesten Stand sind, klicken Sie hier, um weitere Informationen zu erhalten.
React Hooks ermöglichen es, Muster wie HOC und Render Prop endgültig aufzugeben. Denn während des Gebrauchs haben sich eine Reihe von Ansprüchen gegen sie angesammelt:
Um nicht unbegründet zu sein, schauen wir uns ein Beispiel an, wie React Hooks besser (oder vielleicht schlechter) ist. Wir werden Render Prop und nicht HOC betrachten, da sie in der Implementierung sehr ähnlich sind und HOC mehr Nachteile hat. Versuchen wir, ein Dienstprogramm zu schreiben, das die Datenanforderung an die API verarbeitet. Ich bin sicher, dass viele dies hunderte Male in ihrem Leben geschrieben haben. Mal sehen, ob es noch besser und einfacher möglich ist.
Hierfür verwenden wir die beliebte Axios-Bibliothek. Im einfachsten Szenario müssen Sie die folgenden Zustände verarbeiten:
- Datenerfassungsprozess (isFetching)
- Daten erfolgreich empfangen (responseData)
- Fehler beim Empfangen von Daten (Fehler)
- Abbrechen der Anfrage, wenn sich im Laufe ihrer Ausführung die Anforderungsparameter geändert haben, und eine neue
- Abbrechen einer Anforderung, wenn sich diese Komponente nicht mehr im DOM befindet
1. Einfaches Szenario
Wir werden den Standardstatus und eine Funktion (Reduzierung) schreiben, die den Status abhängig vom Ergebnis der Anforderung ändert: Erfolg / Fehler.
Was ist Reduzierer?Als Referenz. Reducer kam zu uns aus der funktionalen Programmierung und für die meisten JS-Entwickler von Redux. Dies ist eine Funktion, die einen vorherigen Status und eine Aktion ausführt und den nächsten Status zurückgibt.
const defaultState = { responseData: null, isFetching: true, error: null }; function reducer1(state, action) { switch (action.type) { case "fetched": return { ...state, isFetching: false, responseData: action.payload }; case "error": return { ...state, isFetching: false, error: action.payload }; default: return state; } }
Wir verwenden diese Funktion in zwei Ansätzen.
Render Prop
class RenderProp1 extends React.Component { state = defaultState; axiosSource = null; tryToCancel() { if (this.axiosSource) { this.axiosSource.cancel(); } } dispatch(action) { this.setState(prevState => reducer(prevState, action)); } fetch = () => { this.tryToCancel(); this.axiosSource = axios.CancelToken.source(); axios .get(this.props.url, { cancelToken: this.axiosSource.token }) .then(response => { this.dispatch({ type: "fetched", payload: response.data }); }) .catch(error => { this.dispatch({ type: "error", payload: error }); }); }; componentDidMount() { this.fetch(); } componentDidUpdate(prevProps) { if (prevProps.url !== this.props.url) { this.fetch(); } } componentWillUnmount() { this.tryToCancel(); } render() { return this.props.children(this.state); }
Haken reagieren
const useRequest1 = url => { const [state, dispatch] = React.useReducer(reducer, defaultState); React.useEffect(() => { const source = axios.CancelToken.source(); axios .get(url, { cancelToken: source.token }) .then(response => { dispatch({ type: "fetched", payload: response.data }); }) .catch(error => { dispatch({ type: "error", payload: error }); }); return source.cancel; }, [url]); return [state]; };
Über die URL erhalten wir aus der verwendeten Komponente die Daten - axios.get (). Wir verarbeiten Erfolg und Irrtum und ändern den Status durch Versand (Aktion). Rückgabestatus an die Komponente. Vergessen Sie nicht, die Anforderung abzubrechen, wenn sich die URL ändert oder wenn die Komponente aus dem DOM entfernt wird. Es ist einfach, aber Sie können auf verschiedene Arten schreiben. Wir heben die Vor- und Nachteile der beiden Ansätze hervor:
Mit React Hooks können Sie weniger Code schreiben, und dies ist eine unbestreitbare Tatsache. Dies bedeutet, dass die Effektivität von Ihnen als Entwickler wächst. Aber Sie müssen ein neues Paradigma beherrschen.
Wenn es Namen von Komponentenlebenszyklen gibt, ist alles sehr klar. Zuerst erhalten wir die Daten, nachdem die Komponente auf dem Bildschirm angezeigt wurde (componentDidMount), dann erneut, wenn sich props.url geändert hat. Vorher vergessen wir nicht, die vorherige Anforderung abzubrechen (componentDidUpdate), wenn die Komponente aus dem DOM entfernt wurde, und dann die Anforderung abzubrechen (componentWillUnmount). .
Aber jetzt verursachen wir einen Nebeneffekt direkt im Rendering. Uns wurde beigebracht, dass dies nicht möglich ist. Obwohl aufhören, nicht wirklich im Render. Und innerhalb der useEffect-Funktion, die nach jedem Rendern asynchron etwas ausführt oder vielmehr das neue DOM festschreibt und rendert.
Wir brauchen dies jedoch nicht nach jedem Rendern, sondern nur beim ersten Rendern und im Falle einer Änderung der URL, die wir als zweites Argument für useEffect angeben.
Neues ParadigmaUm zu verstehen, wie React Hooks funktionieren, müssen Sie sich neuer Dinge bewusst sein. Zum Beispiel der Unterschied zwischen den Phasen: Festschreiben und Rendern. In der Renderphase berechnet React, welche Änderungen im DOM angewendet werden sollen, indem es mit dem Ergebnis des vorherigen Renderings verglichen wird. In der Festschreibungsphase wendet React diese Änderungen auf das DOM an. In der Festschreibungsphase werden die Methoden aufgerufen: componentDidMount und componentDidUpdate. Was jedoch in useEffect geschrieben ist, wird nach dem Festschreiben asynchron aufgerufen und blockiert daher das DOM-Rendering nicht, wenn Sie sich plötzlich versehentlich dazu entschließen, viele Dinge im Nebeneffekt zu synchronisieren.
Fazit - benutze useEffect. Weniger und sicherer schreiben.
Und noch eine großartige Funktion: useEffect kann nach dem vorherigen Effekt und nach dem Entfernen der Komponente aus dem DOM bereinigt werden. Vielen Dank an Rx, der das React-Team für diesen Ansatz inspiriert hat.
Die Verwendung unseres Dienstprogramms mit React Hooks ist auch viel bequemer.
const AvatarRenderProp1 = ({ username }) => ( <RenderProp url={`https://api.github.com/users/${username}`}> {state => { if (state.isFetching) { return "Loading"; } if (state.error) { return "Error"; } return <img src={state.responseData.avatar_url} alt="avatar" />; }} </RenderProp> );
const AvatarWithHook1 = ({ username }) => { const [state] = useRequest(`https://api.github.com/users/${username}`); if (state.isFetching) { return "Loading"; } if (state.error) { return "Error"; } return <img src={state.responseData.avatar_url} alt="avatar" />; };
Die Option "Haken reagieren" sieht wieder kompakter und offensichtlicher aus.
Nachteile Render Prop:
1) Es ist unklar, ob Layout hinzugefügt wird oder nur Logik
2) Wenn Sie den Status von Render Prop im lokalen Status oder in den Lebenszyklen der untergeordneten Komponente verarbeiten müssen, müssen Sie eine neue Komponente erstellen
Fügen Sie eine neue Funktionalität hinzu - Empfangen von Daten mit neuen Parametern durch Benutzeraktion. Ich wollte zum Beispiel einen Button, der einen Avatar Ihres Lieblingsentwicklers erhält.
2) Aktualisieren von Benutzeraktionsdaten
Fügen Sie eine Schaltfläche hinzu, die eine Anfrage mit einem neuen Benutzernamen sendet. Die einfachste Lösung besteht darin, den Benutzernamen im lokalen Status der Komponente zu speichern und den neuen Benutzernamen aus dem Status zu übertragen, nicht aus den aktuellen Requisiten. Aber dann haben wir Copy-Paste, wo immer wir ähnliche Funktionen benötigen. Deshalb haben wir diese Funktionalität in unser Dienstprogramm aufgenommen.
Wir werden es so verwenden:
const Avatar2 = ({ username }) => { ... <button onClick={() => update("https://api.github.com/users/NewUsername")} > Update avatar for New Username </button> ... };
Lassen Sie uns eine Implementierung schreiben. Nachfolgend sind nur die Änderungen gegenüber der Originalversion aufgeführt.
function reducer2(state, action) { switch (action.type) { ... case "update url": return { ...state, isFetching: true, url: action.payload, defaultUrl: action.payload }; case "update url manually": return { ...state, isFetching: true, url: action.payload, defaultUrl: state.defaultUrl }; ... } }
Render Prop
class RenderProp2 extends React.Component { state = { responseData: null, url: this.props.url, defaultUrl: this.props.url, isFetching: true, error: null }; static getDerivedStateFromProps(props, state) { if (state.defaultUrl !== props.url) { return reducer(state, { type: "update url", payload: props.url }); } return null; } ... componentDidUpdate(prevProps, prevState) { if (prevState.url !== this.state.url) { this.fetch(); } } ... update = url => { this.dispatch({ type: "update url manually", payload: url }); }; render() { return this.props.children(this.state, this.update); } }
Haken reagieren
const useRequest2 = url => { const [state, dispatch] = React.useReducer(reducer, { url, defaultUrl: url, responseData: null, isFetching: true, error: null }); if (url !== state.defaultUrl) { dispatch({ type: "update url", payload: url }); } React.useEffect(() => { …(fetch data); }, [state.url]); const update = React.useCallback( url => { dispatch({ type: "update url manually", payload: url }); }, [dispatch] ); return [state, update]; };
Wenn Sie sich den Code genau angesehen haben, haben Sie Folgendes bemerkt:
- Die URL wurde in unserem Dienstprogramm gespeichert.
- defaultUrl schien zu identifizieren, dass die URL über Requisiten aktualisiert wurde. Wir müssen die Änderung von props.url überwachen, sonst wird keine neue Anfrage gesendet.
- fügte die Aktualisierungsfunktion hinzu, die wir an die Komponente zurücksenden, um eine neue Anfrage zu senden, indem wir auf die Schaltfläche klicken.
Beachten Sie, dass wir mit Render Prop getDerivedStateFromProps verwenden mussten, um den lokalen Status zu aktualisieren, falls sich props.url ändert. Und mit React Hooks keine neuen Abstraktionen können Sie sofort die Statusaktualisierung im Render aufrufen - Hurra, Genossen, endlich!
Die einzige Komplikation bei React Hooks bestand darin, die Aktualisierungsfunktion so zu speichern, dass sie sich zwischen den Komponentenaktualisierungen nicht ändert. Wenn die Aktualisierungsfunktion wie in Render Prop eine Klassenmethode ist.
3) Abrufen der API im gleichen Intervall oder Abrufen
Fügen wir eine weitere beliebte Funktion hinzu. Manchmal müssen Sie die API ständig abfragen. Sie wissen nie, dass Ihr Lieblingsentwickler das Profilbild geändert hat, und Sie wissen es nicht. Fügen Sie den Intervallparameter hinzu.
Verwendung:
const AvatarRenderProp3 = ({ username }) => ( <RenderProp url={`https://api.github.com/users/${username}`} pollInterval={1000}> ...
const AvatarWithHook3 = ({ username }) => { const [state, update] = useRequest( `https://api.github.com/users/${username}`, 1000 ); ...
Implementierung:
function reducer3(state, action) { switch (action.type) { ... case "poll": return { ...state, requestId: state.requestId + 1, isFetching: true }; ... } }
Render Prop
class RenderProp3 extends React.Component { state = { ... requestId: 1, } ... timeoutId = null; ... tryToClearTimeout() { if (this.timeoutId) { clearTimeout(this.timeoutId); } } poll = () => { this.tryToClearTimeout(); this.timeoutId = setTimeout(() => { this.dispatch({ type: 'poll' }); }, this.props.pollInterval); }; ... componentDidUpdate(prevProps, prevState) { ... if (this.props.pollInterval) { if ( prevState.isFetching !== this.state.isFetching && !this.state.isFetching ) { this.poll(); } if (prevState.requestId !== this.state.requestId) { this.fetch(); } } } componentWillUnmount() { ... this.tryToClearTimeout(); } ...
Haken reagieren
const useRequest3 = (url, pollInterval) => { const [state, dispatch] = React.useReducer(reducer, { ... requestId: 1, }); React.useEffect(() => { …(fetch data) }, [state.url, state.requestId]); React.useEffect(() => { if (!pollInterval || state.isFetching) return; const timeoutId = setTimeout(() => { dispatch({ type: "poll" }); }, pollInterval); return () => { clearTimeout(timeoutId); }; }, [pollInterval, state.isFetching]); ... }
Eine neue Requisite ist erschienen - pollInterval. Nach Abschluss der vorherigen Anforderung über setTimeout erhöhen wir requestId. Mit Hooks haben wir einen anderen useEffect, in dem wir setTimeout aufrufen. Und unser alter useEffect, der die Anfrage sendet, begann eine andere Variable zu überwachen - requestId, die uns mitteilt, dass setTimeout funktioniert hat, und es ist Zeit, die Anfrage nach einem neuen Avatar zu senden.
In Render Prop musste ich schreiben:
- Vergleich früherer und neuer requestId- und isFetching-Werte
- Clear TimeoutId an zwei Stellen
- Fügen Sie der Klasse die Eigenschaft timeoutId hinzu
Mit React Hooks können Sie kurz und klar schreiben, was wir früher ausführlicher beschrieben haben und was nicht immer klar ist.
4) Was kommt als nächstes?
Wir können die Funktionalität unseres Dienstprogramms weiter erweitern: verschiedene Konfigurationen von Abfrageparametern akzeptieren, Daten zwischenspeichern, eine Antwort und Fehler konvertieren, Daten mit denselben Parametern zwangsweise aktualisieren - Routineoperationen in jeder großen Webanwendung. Bei unserem Projekt haben wir dies vor langer Zeit in eine separate (Aufmerksamkeit!) Komponente herausgenommen. Ja, weil es ein Render Prop war. Mit der Veröffentlichung von Hooks haben wir die Funktion (useAxiosRequest) neu geschrieben und sogar einige Fehler in der alten Implementierung gefunden. Sie können hier sehen und versuchen.