Mit Rückrufen in React arbeiten

  • Tutorial

Bei meiner Arbeit stieß ich gelegentlich auf die Tatsache, dass Entwickler nicht immer klar verstehen, wie der Datentransfermechanismus durch Requisiten, insbesondere Rückrufe, funktioniert und warum ihre PureComponents so oft aktualisiert werden.


In diesem Artikel werden wir daher verstehen, wie Rückrufe an React übermittelt werden, und auch die Besonderheiten der Arbeit von Ereignishandlern besprechen.


TL; DR


  1. Interagieren Sie nicht mit JSX und der Geschäftslogik - dies wird die Codewahrnehmung komplizieren.
  2. Bei kleinen Optimierungen zwischenspeichern Sie Funktionshandler in Form von classProperties für Klassen oder mit useCallback für Funktionen - dann werden reine Komponenten nicht immer gerendert. Insbesondere das Zwischenspeichern von Rückrufen kann hilfreich sein, so dass bei der Übertragung an PureComponent keine unnötigen Aktualisierungszyklen auftreten.
  3. Vergessen Sie nicht, dass Sie in Kolbek kein echtes Ereignis, sondern ein synthetisches Ereignis erhalten. Wenn Sie die aktuelle Funktion verlassen, können Sie nicht auf die Felder dieses Ereignisses zugreifen. Zwischenspeichern Sie die erforderlichen Felder, wenn Sie asynchron mit Schließungen arbeiten.

Teil 1. Event-Handler, Caching und Codewahrnehmung


React ist eine recht bequeme Möglichkeit, Event-Handler für HTML-Elemente hinzuzufügen.


Dies ist eines der grundlegenden Dinge, die jeder Entwickler trifft, wenn er mit React zu schreiben beginnt:


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.


Was tun, wenn der Code im Handler immer mehr wird?


Nehmen wir an, durch den Button müssen wir alle diejenigen laden und filtern, die nicht in einem bestimmten Befehl ( user.team === 'search-team') enthalten sind, und sie dann nach Alter sortieren.


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 zu verstehen. Der Code der Geschäftslogik wird mit dem Layout gemischt, das dem Benutzer angezeigt wird.


Der einfachste Weg, das loszuwerden: Setzen 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 vom JSX-Code in ein separates Feld in unserer Klasse übernommen. Um dies in einer Funktion verfügbar zu machen, definieren wir einen Rückruf wie folgt:onClick={() => this.fetchUsers()}


Außerdem können wir bei der Beschreibung einer Klasse 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 Colback als deklarieren onClick={this.fetchUsers}


Was ist der Unterschied zwischen diesen beiden Wegen?


onClick={this.fetchUsers}- Hier wird bei jedem Aufruf der Renderfunktion in props k buttonimmer die gleiche Verbindung übertragen.


Bei onClick={() => this.fetchUsers()}jedem Aufruf der Renderfunktion initialisiert JavaScript die neue Funktion () => this.fetchUsers()und setzt sie auf onClickprop. Das bedeutet , dass nextProp.onClick, und prop.onClickin buttondiesem Fall wird nicht immer gleich sein, auch wenn die Komponente als sauber markiert wird, wird es pererenderen.


Was droht es während der Entwicklung?


In den meisten Fällen werden Sie keinen Leistungsabfall bemerken, da das von der Komponente generierte virtuelle DOM sich nicht vom vorherigen unterscheidet und Ihr DOM nicht geändert wird.


Wenn Sie jedoch große Listen von Komponenten oder Tabellen rendern, werden Sie feststellen, dass eine große Datenmenge "abgebremst" wird.


Warum ist es wichtig zu verstehen, wie eine Funktion auf einen Calbek übertragen wird?


Oft können Sie solche Tipps auf Twitter oder Stackoverflow finden:


"Wenn Sie Leistungsprobleme mit React-Anwendungen haben, versuchen Sie, die Vererbung von Component durch PureComponent zu ersetzen. Vergessen Sie auch nicht, dass CompompetUpdate für Component immer definiert werden kann, um unnötige Aktualisierungszyklen zu vermeiden."


Wenn wir eine Komponente als Pure definieren, bedeutet dies, dass sie bereits eine Funktion hat shouldComponentUpdate, die shallowEqual zwischen Requisiten und nextProps macht.


Jedes Mal, wenn wir eine neue Kolbek-Funktion an eine solche Komponente übergeben, verlieren wir alle Vorteile und Optimierung PureComponent.


Schauen wir uns ein Beispiel an.
Erstellen Sie die Eingabekomponente, die auch Informationen anzeigt, wie oft sie aktualisiert wurden:


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 Sie zwei Komponenten, die die Eingabe nach innen 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 die 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 von Hand ausprobieren: https://codesandbox.io/s/2vwz6kjjkr
Dieses Beispiel zeigt deutlich, 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 Komponentenfunktionen


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


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


Die Eingabe muss Informationen darüber enthalten, wie oft sie geändert wurden. Wenn wir im Fall von Klassen in unserer Instanz ein Feld verwendet haben, auf das der Zugriff dadurch implementiert wurde, können wir im Fall einer Funktion keine Variable hierüber deklarieren.
React bietet einen useRef-Hook, mit dem Sie einen Verweis auf das HtmlElement in der DOM-Baumstruktur speichern können. Es ist jedoch auch interessant, da es für die regulären 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>
    </>
  );
}

Außerdem muss die Komponente "sauber" sein, dh nur dann aktualisiert werden, wenn sich die Requisiten, die an die Komponente übertragen wurden, geändert haben.
Dafür gibt es verschiedene Bibliotheken, die HOC bereitstellen, aber es ist besser, die Memo-Funktion zu verwenden, die bereits in React integriert ist, 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 Input-Komponente ist fertig, jetzt schreiben wir die Komponenten A und B neu.
Bei Komponente B ist dies einfach möglich:


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 einen useStateHook verwendet, mit dem Sie die Zustandskomponente speichern und bearbeiten können, falls die Komponente durch eine Funktion dargestellt wird.


Wie können wir den Callback der Funktion zwischenspeichern? Wir können es nicht aus der Komponente entfernen, da es in diesem Fall für verschiedene Komponenteninstanzen üblich ist.
Für solche Aufgaben verfügt React über eine Reihe von Caching- und Memory-Hooks, von denen https://reactjs.org/docs/hooks-reference.html für uns am besten geeignet ist.useCallback


Fügen Sie Adiesen Haken der Komponente hinzu :


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

Die Funktion wird zwischengespeichert, sodass die Input-Komponente nicht jedes Mal aktualisiert wird.


Wie funktioniert useCallbackHook?


Dieser Hook gibt die zwischengespeicherte Funktion zurück (dh der Link ändert sich nicht vom Renderer zum Renderer).
Neben der Funktion, die zwischengespeichert werden muss, wird das zweite Argument an sie übergeben - ein leeres Array.
Mit diesem Array können Sie beim Ändern eine Liste von Feldern übertragen, deren Funktion Sie ändern möchten einen neuen Link zurückgeben


Der Unterschied zwischen der üblichen Art der Übertragung der Funktion auf den Rückruf useCallbackist offensichtlich und kann hier gefunden werden: https://codesandbox.io/s/0y7wm3pp1w


Warum brauchen wir ein Array?


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


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 der Requisite ab a. Wenn wir das Beispiel ausführen, funktioniert alles korrekt, bis wir am Ende hinzufügen:


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

Nachdem das Timeout ausgelöst wurde, wird ein Klick auf die Schaltfläche in der Warnmeldung angezeigt 1. Dies geschieht, weil wir die vorherige Funktion gespeichert haben, die die aVariable geschlossen hat . Da es sich aum eine Variable handelt, die in unserem Fall der Werttyp ist und der Werttyp unveränderlich ist, haben wir diesen Fehler erhalten. Wenn wir den Kommentar entfernen /*a*/, funktioniert der Code korrekt. React im zweiten Rendering überprüft, ob die im Array übergebenen Daten unterschiedlich sind, und gibt eine neue Funktion zurück.


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


Die React viele Funktionen vorgestellt , die memoizirovat Daten, zum Beispiel erlauben useRef, useCallbackund useMemo.
Wenn letzteres zum Speichern der Werte einer Funktion benötigt wird und diese useCallbackeinander sehr ähnlich sind, useRefkönnen Sie nicht nur Verweise auf DOM-Elemente zwischenspeichern, sondern auch als Instanzfeld fungieren.


Es kann auf den ersten Blick zum Zwischenspeichern von Funktionen verwendet werden, da es useRefauch Daten zwischen einzelnen Komponentenaktualisierungen zwischenspeichert.
Es ist jedoch nicht useRefwünschenswert, Funktionen zum Zwischenspeichern zu verwenden. Wenn unsere Funktion eine Schließung verwendet, kann sich der geschlossene Wert bei jedem Rendering ändern und unsere zwischengespeicherte Funktion arbeitet mit dem alten Wert. Dies bedeutet, dass wir die Aktualisierungslogik der Funktionen schreiben oder einfach verwenden müssen useCallback, in der sie durch den Abhängigkeitsmechanismus implementiert wird.


https://codesandbox.io/s/p70pprpvvx Hier können Sie die Speicherung von Funktionen mit rechts useCallback, mit falsch und mit sehen useRef.


Teil 3. Synthetische Ereignisse


Wir haben bereits herausgefunden, wie Event-Handler verwendet werden und wie mit Closures in Rückrufen korrekt umgegangen wird. React hat jedoch einen weiteren wichtigen Unterschied, wenn mit ihnen gearbeitet wird:


Lassen Sie uns das heute beachten Input, wir aufgearbeitet, in perfekter Synchronisation, aber in einigen Fällen müssen möglicherweise verzögert Rückruf, strukturiert sein debounce oder Drosselung . Zum Beispiel ist das Entprellen sehr praktisch für die Eingabezeilen der Suchzeile. Die Suche wird nur ausgeführt, wenn der Benutzer die Zeicheneingabe beendet.


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


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 React intern Ereignisse proxysiert und das so genannte Syntetic Event in unsere onChange gerät, die nach unserer Funktion "gelöscht" wird (die Felder werden mit Null markiert). React tut dies aus Leistungsgründen, um ein einzelnes Objekt zu verwenden, anstatt jedes Mal ein neues zu erstellen.


Wenn Sie, wie in diesem Beispiel, einen Wert annehmen müssen, reicht es aus, die erforderlichen Felder vor dem Beenden der Funktion zwischenzuspeichern:


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 eines Ereignisses gespeichert werden. Dazu können Sie aufrufen, event.persist()dass die
angegebene Syntetic-Ereignisinstanz aus dem Ereignispool der Reaktorereignisse entfernt wird.


Fazit:


Event-Handler für Reaktionen sind sehr praktisch, da sie:


  1. Automatisieren Sie das Abonnement und das Abonnement (mit der Unmount-Komponente).
  2. Die Wahrnehmung des Codes wird vereinfacht, die meisten Abonnements lassen sich im JSX-Code leicht nachverfolgen.

Bei der Entwicklung von Anwendungen können Sie jedoch auf einige Schwierigkeiten stoßen:


  1. Callbacks in Requisiten neu definieren;
  2. Synthetische Ereignisse, die nach der aktuellen Funktion gelöscht werden.

Eine erneute Definition von Rückrufen ist normalerweise nicht erkennbar, da sich vDOM nicht ändert. Es ist jedoch zu beachten, dass Sie Optimierungen einführen, Komponenten mit Pure durch Vererbung ersetzen PureComponentoder verwenden memo. Dann sollten Sie sich um das Cachen kümmern, da sonst die Vorteile der Einführung von PureComponents oder Memo nicht spürbar sind. Für das Caching können Sie entweder classProperties (beim Arbeiten mit einer Klasse) oder useCallbackHook (beim Arbeiten mit Funktionen) verwenden.


Wenn Sie für den richtigen asynchronen Betrieb Daten aus dem Ereignis benötigen, zwischenspeichern Sie auch die benötigten Felder.


Jetzt auch beliebt: