Poprawne bindowanie funkcji w React.js

Ten wpis jest 16 częścią z 32 w kursie React.js

Jak pewnie zauważyłaś/eś — wywołanie metody klasy w React.js z poziomu funkcji render kończy się źle, o ile nie użyjesz bind. Wspominałem też o tym w kursie i sam używałem po prostu bind w czasie renderowania. Ale czy to dobre rozwiązanie? Co z wydajnością i czytelnością takiego kodu?

bind w ogóle

Zacznijmy może w ogóle od tego po co Ci bind i co ta funkcja robi 😉 Otóż w JS-ie metody tak naprawdę nie są metodami, tylko funkcjami. Pewnie myślisz: cooo, co to za różnica? Już wyjaśniam, najlepiej na prostym przykładzie:

const obj = {
  value: 'some value',
  method() { return this.value; }
};
obj.method(); // "some value"

const x = obj.method;
x(); // undefined

Czemu tak się dzieje? Po przypisaniu do nowej zmiennej, funkcja method „nie pamięta” już, że była kiedyś częścią obiektu i wewnątrz niej jej this się zmienia — nie wskazuje już na obiekt. Więcej o tym możesz doczytać tutaj:

this w JS — czyli kilka słów o kontekście wywołania funkcji

Jak to się ma do React.js

Ale w React.js zawsze używasz {this.myFunction} więc mogłoby by się wydawać, że kontekst powinien być zachowany, no nie? Pomyśl o tym (i przeczytaj linkowany wyżej artykuł). Nie wywołujesz tej funkcji w tym miejscu, tylko przekazujesz this.myFunction do atrybutu… to tak jakbyś zrobił(a) const prop = this.myFunction a następnie wywołał(a) prop(…) — oryginalny kontekst jest gubiony.

Co z tym zrobić?

Funkcję możemy sobie zbindować do konkretnego kontekstu. Dokładnie tak jak pokazywałem wcześniej w wielu przykładach. Robiłem to tak:

<input onInput={this.filterUsers.bind(this)} />

Ale to nie jest najlepsze rozwiązanie. Jest przynajmniej kilka powodów:

  1. Ta składnia powoduje, że przy każdym renderze Tworzona jest nowa funkcja. To może być problem dla wydajności, szczególnie gdy budujesz coś skomplikowanego albo renderujesz 30+ razy na sekundę. A nawet jeśli nie, to nadal Twój instynkt powinien Ci podpowiadać: „Po co to robić? To niepotrzebne.”
  2. Z faktu, że tworzona jest nowa funkcja wynika też pewien problem specyficzny dla Reacta — od razu unicestwia to wszelkie automatyczne mechanizmy poprawiające wydajność komponentów! A to już może być problem. shouldComponentUpdate i PureComponent (o których będę pisał w przyszłości) nie poradzą sobie z bind w render. Tworzona jest nowa funkcja, więc dla Reacta wygląda to tak, jakby to była inna funkcja — a więc renderuje on cały komponent na nowo. Za każdym razem.
  3. Nie ma punktu trzeciego 😉

Najlepiej więc poznać od razu dobre praktyki i je wprowadzić w życie. Czym skorupka za młodu nasiąknie…

Bind w konstruktorze

Jednym z rozwiązań jest wykonywanie bind w konstruktorze klasy. Jest to popularne wyjście z sytuacji chyba głównie dlatego, że sposób, który opiszę dalej (moim zdaniem lepszy) nie jest jeszcze oficjalnie w specyfikacji ECMAScript — jest nadal tylko szkicem roboczym. W każdym razie, bind w konstruktorze polega na nadpisaniu metody przy pomocy zbindowanej funkcji. Na przykład o tak:

class App extends React.Component {
  constructor() {
    super();

    this.filterUsers = this.filterUsers.bind(this); // tutaj bind!
  }

  filterUsers(e) {
    ……
  }

  render() {
    return (
      <div>
        <input onInput={this.filterUsers} />
      </div>
    );
  }
};

W ten sposób nie musisz już używać bind w renderze, a Twoja funkcja pozostaje niezmienna od powstania komponentu aż do jego zniszczenia. To rozwiązuje problem. Ale jest brzydkie. I trzeba o tym pamiętać.

Arrow function

Znasz funkcje strzałkowe, prawda? Unikalną cechą tych funkcji jest to, że posiadają leksykalne this, a więc są (tak jakby) automatycznie zbindowane. To upraszcza sprawę. Możesz ich użyć w render i to zadziała:

<input onInput={(e) => this.filterUsers(e)} />

Ale mamy tutaj znowu problemy z początku artykułu: Przy każdym renderze tworzona jest nowa funkcja. Tego nie chcesz. Dodatkowo trzeba pamiętać, aby przekazać wszystkie argumenty z jednej funkcji do drugiej… a to jest co najmniej niewygodne.

Arrow function x 2

No i w końcu dochodzę do mojego ulubionego rozwiązania. Wymaga to użycia funkcji strzałkowej (yay 😁) i własności w klasie, która niestety jest nadal tylko szkicem i nie trafiła jeszcze oficjalnie do ECMAScript (nay 😥).

Nota poboczna: Mówię o specyfikacji „Class Fields & Static Properties”, która szybko raczej nie zostanie ukończona gdyż ostatnio doszło do połączenia jej z „Private Fields Proposal” i powstał wspólny „Class field declarations for JavaScript”. Jest to niby już „stage 3” (z 4 możliwych), ale, sam(a) rozumiesz, sprawa nie jest tak prosta jak się pozornie zdaje…

Jednakże, same „class fields” są zaimplementowane w Babel i powszechnie używane. Tak powszechnie, że są też domyślnie wykorzystywane przez create-react-app! To chyba rozwiązuje problemy, no nie? Nie musisz się tym martwić: Bierz i korzystaj!

Pomysł jest prosty: Zdefiniuj własność w klasie, ale zamiast zwykłej funkcji użyj funkcji strzałkowej! O tak:

class App extends React.Component {
  filterUsers = (e) => {
    ……
  }

  render() {
    return (
      <div>
        <input onInput={this.filterUsers} />
      </div>
    );
  }
};

I już 🙂 To moje ulubione rozwiązanie bo jest proste i nie wymaga dodatkowego kodu. No i działa razem w create-react-app od razu.

Można tak skonfigurować ESLint, aby wyłapywał kiedy używasz zwykłej funkcji zamiast arrow function w klasie — tam gdzie jest to potrzebne.

Podsumowanie

Mam nadzieję, że już rozumiesz naturę problemu. Na pewno potrafisz też już go rozwiązać i znasz wady/zalety poszczególnych sposobów. Ostatni wydaje się wygodny, prawda? 😉

Jeśli chcesz na bieżąco śledzić kolejne części kursu React.js to koniecznie śledź mnie na Facebooku i zapisz się na newsletter.

Ćwiczenie

Ćwiczenie: Przepisz kod aplikacji napisanej w create-react-app tak, aby korzystał z arrow functions. Napisz w komentarzu czy takie rozwiązanie Ci się podoba.