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:
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:
- 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.”
- 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
iPureComponent
(o których będę pisał w przyszłości) nie poradzą sobie zbind
wrender
. 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. - 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? ;) zapisz się na szkolenie z React.
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.