Arbeiten mit Rückrufen in React

Während meiner Arbeit stieß ich regelmäßig auf die Tatsache, dass Entwickler nicht immer klar verstehen, wie der Mechanismus für die Übertragung von Daten über Requisiten, insbesondere Rückrufe, funktioniert und warum ihre PureComponents so oft aktualisiert werden.


In diesem Artikel erfahren Sie daher, wie Rückrufe an React übergeben werden, und erläutern auch die Funktionen von Ereignishandlern.


TL; DR


  1. Beeinträchtigen Sie nicht JSX und die Geschäftslogik - dies erschwert die Wahrnehmung des Codes.
  2. Bei kleinen Optimierungen werden Cache-Handler-Funktionen in Form von classProperties für Klassen oder mit useCallback für Funktionen verwendet - dann werden reine Komponenten nicht ständig gerendert. Insbesondere das Caching von Rückrufen kann nützlich sein, damit bei der Übergabe an die PureComponent keine unnötigen Aktualisierungszyklen auftreten.
  3. Vergessen Sie nicht, dass Sie im Rückruf kein echtes Ereignis erhalten, sondern ein synthetisches Ereignis. Wenn Sie die aktuelle Funktion verlassen, können Sie nicht auf die Felder dieses Ereignisses zugreifen. Zwischenspeichern Sie die Felder, die Sie benötigen, wenn Sie asynchrone Abschlüsse haben.

Teil 1. Event-Handler, Caching und Code-Wahrnehmung


React bietet eine recht bequeme Möglichkeit, Ereignishandler für HTML-Elemente hinzuzufügen.


Dies ist eines der grundlegenden Dinge, die Entwickler kennenlernen müssen, wenn sie mit dem Schreiben in React beginnen:


class MyComponent extends Component { render() { return <button onClick={() => console.log('Hello world!')}>Click me</button>; } } 

Einfach genug? Aus diesem Code wird sofort klar, was passiert, wenn der Benutzer auf die Schaltfläche klickt.


Aber was ist, wenn der Code im Handler immer mehr wird?


Angenommen, wir klicken auf die Schaltfläche, um alle zu laden und herauszufiltern, die nicht zu einem bestimmten Team gehören ( user.team === 'search-team' ), und sortieren sie dann nach Alter.


 class MyComponent extends Component { constructor(props) { super(props); this.state = { users: [] }; } render() { return ( <div> <ul> {this.state.users.map(user => ( <li>{user.name}</li> ))} </ul> <button onClick={() => { console.log('Hello world!'); window .fetch('/usersList') .then(result => result.json()) .then(data => { const users = data .filter(user => user.team === 'search-team') .sort((a, b) => { if (a.age > b.age) { return 1; } if (a.age < b.age) { return -1; } return 0; }); this.setState({ users: users, }); }); }} > Load users </button> </div> ); } } 

Dieser Code ist ziemlich schwer herauszufinden. Der Geschäftslogikcode wird mit dem Layout gemischt, das der Benutzer sieht.


Der einfachste Weg, dies loszuwerden: Bringen Sie die Funktion auf die Ebene der Klassenmethoden:


 class MyComponent extends Component { fetchUsers() { //    } render() { return ( <div> <ul> {this.state.users.map(user => ( <li>{user.name}</li> ))} </ul> <button onClick={() => this.fetchUsers()}>Load users</button> </div> ); } } 

Hier haben wir die Geschäftslogik von JSX-Code in ein separates Feld in unserer Klasse verschoben. Um dies innerhalb der Funktion zugänglich zu machen, haben wir den Rückruf folgendermaßen definiert: onClick={() => this.fetchUsers()}


Wenn wir eine Klasse beschreiben, können wir außerdem ein Feld als Pfeilfunktion deklarieren:


 class MyComponent extends Component { fetchUsers = () => { //    }; render() { return ( <div> <ul> {this.state.users.map(user => ( <li>{user.name}</li> ))} </ul> <button onClick={this.fetchUsers}>Load users</button> </div> ); } } 

Dadurch können wir den Rückruf als onClick={this.fetchUsers}


Was ist der Unterschied zwischen diesen beiden Methoden?


onClick={this.fetchUsers} - Hier wird bei jedem Aufruf der onClick={this.fetchUsers} in Requisiten immer derselbe Link an die button gesendet.


Im Fall von onClick={() => this.fetchUsers()} initialisiert JavaScript bei jedem onClick={() => this.fetchUsers()} der onClick={() => this.fetchUsers()} eine neue Funktion () => this.fetchUsers() und setzt sie auf onClick prop. Dies bedeutet, dass nextProp.onClick und prop.onClick auf die button in diesem Fall immer nicht gleich sind. Selbst wenn die Komponente als sauber markiert ist, wird sie gerendert.


Was bedroht dies mit der Entwicklung?


In den meisten Fällen werden Sie visuell keinen Leistungsabfall bemerken, da sich das von der Komponente generierte virtuelle DOM nicht vom vorherigen unterscheidet und es keine Änderungen in Ihrem DOM gibt.


Wenn Sie jedoch große Listen von Komponenten oder Tabellen rendern, werden Sie bei einer großen Datenmenge "Bremsen" bemerken.


Warum ist es wichtig zu verstehen, wie eine Funktion auf den Rückruf übertragen wird?


Oft kann man auf Twitter oder auf Stackoverflow auf solche Tipps stoßen:


"Wenn Sie Leistungsprobleme mit React-Anwendungen haben, versuchen Sie, die Vererbung von Component durch PureComponent zu ersetzen. Denken Sie auch daran, dass Sie für Component immer shouldComponentUpdate definieren können, um unnötige Aktualisierungsschleifen zu vermeiden."


Wenn wir eine Komponente als Pure definieren, bedeutet dies, dass sie bereits über eine shouldComponentUpdate Funktion verfügt, die flatEqual zwischen Requisiten und nextProps ausführt.


Wenn wir jedes Mal eine neue Rückruffunktion an eine solche Komponente übergeben, verlieren wir alle Vorteile und Optimierungen von PureComponent .


Schauen wir uns ein Beispiel an.
Erstellen Sie eine Eingabekomponente, die auch Informationen darüber anzeigt, wie oft sie aktualisiert wurde:


 class Input extends PureComponent { renderedCount = 0; render() { this.renderedCount++; return ( <div> <input onChange={this.props.onChange} /> <p>Input component was rerendered {this.renderedCount} times</p> </div> ); } } 

Erstellen wir zwei Komponenten, die die Eingabe intern rendern:


 class A extends Component { state = { value: '' }; onChange = e => { this.setState({ value: e.target.value }); }; render() { return ( <div> <Input onChange={this.onChange} /> <p>The value is: {this.state.value} </p> </div> ); } } 

Und der zweite:


 class B extends Component { state = { value: '' }; onChange(e) { this.setState({ value: e.target.value }); } render() { return ( <div> <Input onChange={e => this.onChange(e)} /> <p>The value is: {this.state.value} </p> </div> ); } } 

Sie können das Beispiel hier mit Ihren Händen ausprobieren: https://codesandbox.io/s/2vwz6kjjkr
Dieses Beispiel zeigt, wie Sie alle Vorteile von PureComponent verlieren können, wenn Sie jedes Mal eine neue Rückruffunktion an PureComponent übergeben.


Teil 2. Verwenden von Ereignishandlern in Funktionskomponenten


In der neuen Version von React (16.8) wurde der React-Hooks- Mechanismus angekündigt, mit dem Sie vollwertige Funktionskomponenten mit einem klaren Lebenszyklus schreiben können, der fast alle Benutzerfälle abdecken kann, die bisher nur Klassen abdeckten.


Wir modifizieren das Beispiel mit der Input-Komponente so, dass alle Komponenten durch eine Funktion dargestellt werden und arbeiten mit React-Hooks.


Die Eingabe muss Informationen darüber in sich speichern, wie oft sie geändert wurde. Wenn wir in unserer Instanz ein Feld verwendet haben, auf das der Zugriff dadurch implementiert wurde, können wir im Fall einer Funktion keine Variable dadurch deklarieren.
React bietet einen useRef-Hook, mit dem ein Verweis auf das HtmlElement im DOM-Baum gespeichert werden kann. Er ist jedoch auch interessant, da er für reguläre Daten verwendet werden kann, die unsere Komponente benötigt:


 import React, { useRef } from 'react'; export default function Input({ onChange }) { const componentRerenderedTimes = useRef(0); componentRerenderedTimes.current++; return ( <> <input onChange={onChange} /> <p>Input component was rerendered {componentRerenderedTimes.current} times</p> </> ); } 

Die Komponente muss außerdem "sauber" sein, dh sie wird nur aktualisiert, wenn sich die an die Komponente übergebenen Requisiten geändert haben.
Dafür gibt es verschiedene Bibliotheken, die HOC bereitstellen. Es ist jedoch besser, die bereits in React integrierte Memofunktion zu verwenden, da sie schneller und effizienter arbeitet:


 import React, { useRef, memo } from 'react'; export default memo(function Input({ onChange }) { const componentRerenderedTimes = useRef(0); componentRerenderedTimes.current++; return ( <> <input onChange={onChange} /> <p>Input component was rerendered {componentRerenderedTimes.current} times</p> </> ); }); 

Die Eingabekomponente ist fertig, jetzt schreiben wir die Komponenten A und B neu.
Im Fall von Komponente B ist dies einfach zu tun:


 import React, { useState } from 'react'; function B() { const [value, setValue] = useState(''); return ( <div> <Input onChange={e => setValue(e.target.value)} /> <p>The value is: {value} </p> </div> ); } 

Hier haben wir den useState Hook verwendet, mit dem Sie den Status der Komponente speichern und damit arbeiten können, falls die Komponente durch eine Funktion dargestellt wird.


Wie können wir die Rückruffunktion zwischenspeichern? Wir können es nicht aus der Komponente entfernen, da es in diesem Fall verschiedenen Instanzen der Komponente gemeinsam ist.
Für solche Aufgaben verfügt React über eine Reihe von Caching- und Memoizing-Hooks, von denen useCallback für useCallback am besten geeignet useCallback https://reactjs.org/docs/hooks-reference.html


Fügen Sie diesen Haken zu Komponente A :


 import React, { useState, useCallback } from 'react'; function A() { const [value, setValue] = useState(''); const onChange = useCallback(e => setValue(e.target.value), []); return ( <div> <Input onChange={onChange} /> <p>The value is: {value} </p> </div> ); } 

Wir haben die Funktion zwischengespeichert, was bedeutet, dass die Eingabekomponente nicht jedes Mal aktualisiert wird.


Wie funktioniert useCallback hook?


Dieser Hook gibt eine zwischengespeicherte Funktion zurück (d. H. Der Link ändert sich nicht von Render zu Render).
Zusätzlich zu der zwischenzuspeichernden Funktion wird ein zweites Argument an sie übergeben - ein leeres Array.
Mit diesem Array können Sie beim Ändern eine Liste von Feldern übertragen, die Sie zum Ändern der Funktion benötigen, d. H. einen neuen Link zurückgeben.


Den Unterschied zwischen der üblichen Methode zum Übertragen einer Funktion auf einen Rückruf und useCallback hier: https://codesandbox.io/s/0y7wm3pp1w


Warum brauchen wir ein Array?


Angenommen, wir müssen eine Funktion, die von einem bestimmten Wert abhängt, durch einen Abschluss zwischenspeichern:


 import React, { useCallback } from 'react'; import ReactDOM from 'react-dom'; import './styles.css'; function App({ a, text }) { const onClick = useCallback(e => alert(a), [ /*a*/ ]); return <button onClick={onClick}>{text}</button>; } const rootElement = document.getElementById('root'); ReactDOM.render(<App text={'Click me'} a={1} />, rootElement); 

Hier hängt die App-Komponente von Prop a . Wenn Sie das Beispiel ausführen, funktioniert alles ordnungsgemäß, bis wir es am Ende hinzufügen:


 setTimeout(() => ReactDOM.render(<App text={'Next A'} a={2} />, rootElement), 5000); 

Nachdem das Timeout ausgelöst wurde, wird beim Klicken auf die Schaltfläche in Alarm 1 angezeigt. Dies geschieht, weil wir die vorherige Funktion gespeichert haben, die a Variable geschlossen hat. Und da a eine Variable ist, die in unserem Fall ein Werttyp ist und der Werttyp unveränderlich ist, haben wir diesen Fehler erhalten. Wenn wir den Kommentar /*a*/ entfernen, funktioniert der Code korrekt. Wenn Sie beim zweiten Rendern reagieren, wird überprüft, ob die im Array übergebenen Daten unterschiedlich sind, und es wird eine neue Funktion zurückgegeben.


Sie können dieses Beispiel hier selbst ausprobieren: https://codesandbox.io/s/6vo8jny1ln


React bietet viele Funktionen, mit denen Sie Daten speichern können, z. B. useRef , useCallback und useMemo .
Wenn letzteres zum Speichern des Funktionswerts benötigt wird und sie useRef sehr ähnlich useRef , können Sie mit useRef nicht nur Verweise auf DOM-Elemente zwischenspeichern, sondern auch als useRef fungieren.


Auf den ersten Blick können damit Funktionen zwischengespeichert werden, da useRef auch Daten zwischen separaten Komponentenaktualisierungen zwischenspeichert.
Die Verwendung von useRef zum Zwischenspeichern von Funktionen ist jedoch unerwünscht. Wenn unsere Funktion Closure verwendet, kann sich in jedem Rendering der Closed-Wert ändern, und unsere zwischengespeicherte Funktion funktioniert mit dem alten Wert. Dies bedeutet, dass wir die Funktionsaktualisierungslogik schreiben oder einfach useCallback verwenden useCallback , in dem sie aufgrund des Abhängigkeitsmechanismus implementiert ist.


https://codesandbox.io/s/p70pprpvvx hier können Sie die Speicherung von Funktionen mit dem richtigen useCallback , mit dem falschen und mit useRef .


Teil 3. Syntetische Ereignisse


Wir haben bereits herausgefunden, wie man Ereignishandler verwendet und wie man mit Schließungen in Rückrufen richtig arbeitet, aber in React gibt es einen weiteren sehr wichtigen Unterschied, wenn man mit ihnen arbeitet:


Hinweis: Jetzt ist die Input , mit der wir oben gearbeitet haben, absolut synchron. In einigen Fällen kann es jedoch erforderlich sein, dass der Rückruf je nach Entprellungs- oder Drosselungsmuster verzögert erfolgt. So ist beispielsweise das Entprellen sehr praktisch für die Eingabe von Suchzeichenfolgen. Die Suche wird nur ausgeführt, wenn der Benutzer keine Zeichen mehr eingibt.


Erstellen Sie eine Komponente, die intern eine Statusänderung verursacht:


 function SearchInput() { const [value, setValue] = useState(''); const timerHandler = useRef(); return ( <> <input defaultValue={value} onChange={e => { clearTimeout(timerHandler.current); timerHandler.current = setTimeout(() => { setValue(e.target.value); }, 300); // wait, if user is still writing his query }} /> <p>Search value is {value}</p> </> ); } 

Dieser Code funktioniert nicht. Tatsache ist, dass Reagieren innerhalb von Proxy-Ereignissen und das sogenannte synthetische Ereignis in unseren onChange-Rückruf gelangt, der nach dem "Löschen" unserer Funktion "gelöscht" wird (die Felder werden mit null markiert). Aus Leistungsgründen verwendet React dies, um ein einzelnes Objekt zu verwenden, anstatt jedes Mal ein neues zu erstellen.


Wenn wir wie in diesem Beispiel einen Wert annehmen müssen, reicht es aus, die erforderlichen Felder zwischenzuspeichern, bevor die Funktion beendet wird:


 function SearchInput() { const [value, setValue] = useState(''); const timerHandler = useRef(); return ( <> <input defaultValue={value} onChange={e => { clearTimeout(timerHandler.current); const pendingValue = e.target.value; // cached! timerHandler.current = setTimeout(() => { setValue(pendingValue); }, 300); // wait, if user is still writing his query }} /> <p>Search value is {value}</p> </> ); } 

Ein Beispiel finden Sie hier: https://codesandbox.io/s/oj6p8opq0z


In sehr seltenen Fällen muss die gesamte Instanz des Ereignisses beibehalten werden. Dazu können Sie event.persist() aufrufen, das entfernt wird
Diese Instanz des synthetischen Ereignisses aus dem Ereignispool der Reaktionsereignisse.


Fazit:


Reaktionsereignishandler sind sehr praktisch, da sie:


  1. Abonnieren und Abbestellen automatisieren (mit Unmount-Komponente);
  2. Vereinfachen Sie die Wahrnehmung des Codes. Die meisten Abonnements lassen sich leicht in JSX-Code verfolgen.

Gleichzeitig können bei der Entwicklung von Anwendungen einige Schwierigkeiten auftreten:


  1. Überschreiben von Rückrufen in Requisiten;
  2. Synthetische Ereignisse, die nach Ausführung der aktuellen Funktion gelöscht werden.

Das Überschreiben von Rückrufen ist normalerweise nicht erkennbar, da sich vDOM nicht ändert. Beachten Sie jedoch, dass Sie beim Einführen von Optimierungen, Ersetzen von Komponenten durch Pure durch Vererbung von PureComponent oder Verwenden von memo PureComponent sollten, diese zwischenzuspeichern, da sonst die Vorteile der Einführung von PureComponents oder Memo nicht erkennbar sind. Zum Zwischenspeichern können Sie entweder classProperties (bei der Arbeit mit einer Klasse) oder den useCallback Hook (bei der Arbeit mit Funktionen) verwenden.


Wenn Sie für einen korrekten asynchronen Betrieb Daten von einem Ereignis benötigen, müssen Sie auch die benötigten Felder zwischenspeichern.

Source: https://habr.com/ru/post/de439138/


All Articles