Część II. Komponenty.
Tworzenie i renderowanie komponentów
Przyjrzyjmy się bliżej konstrukcji opisanych we wprowadzeniu komponentów.
Utwórzmy w tym celu nowy komponent React, a następnie go werenderujmy (w ReactDOM):
import React from 'react' ;
import ReactDOM from 'react-dom' ;
class MyComponent extends React.Component {
render() {
return <div> Hello World! </div> ;
}
}
ReactDOM.render( <MyComponent/> , document.getElementById('root'));
Można sądzić, że podczas render() używamy MyComponent, czyli instancji klasy MyComponent (czyli new MyComponent()
). Chociaż brzmi to rozsądnie, rzeczywistość procesu jest nieco bardziej złożona.
W rzeczywistości procesor JSX (babel) konwertuje linię<MyComponent />
, aby użyć React.createElement
do wygenerowania instancji. Ten wygenerowany element jest przekazywany do metodyrender()
obiektu ReactDOM
// generated code post-JSX processing
ReactDOM.render( React.createElement(MyComponent, null ),
document.getElementById( 'root' )
);
Element React
Element React to tak naprawdę opis tego, co ostatecznie zostanie użyte do wygenerowania interfejsu użytkownika – czyli elementy VirtualDOM. Element React ma cztery właściwości: typ, własności, klucz i ref. Nie ma żadnych metod!
Atrybuty i właściwości
Znaczniki HTML mają własności. Na przykład znacznik <a> ma własność href (<a href=”http://...”>; ...). W React znacznikom odpowiadają obiekty / klasy obiektów (1). Możemy też definiować własne klasy z własnościami używanymi w renderowaniu.
Przykład [011]:
class Tekst extends Component {
render() {
return (
<span>{this.props.normal} <i>{this.props.italic}</i></span>
);
}
}
Objaśnienie:
Przedrostek this. oznacza "ten obiekt" - czyli odnosimy się wewnątrz obiektu do jego własności (props). Własności this.pros.normal i this.props.italic są definiowane przez nas (tekst normalny i kursywą).
Własności nie muszą być deklarowane - wystarczy je używać i pamiętać o zainicjowaniu.
Jak to zrobić? W trakcie wywołania. Tekst zapisany kursywą:<Tekst italic='abc' />
Odwołanie do kodu JavaScript (tu: własności) w renderowanym kodzie umieszcza się w nawiasach {}.
Zmiana stanu komponentu
Własności obiektów gromadzone w props nie mogą być zmieniane wewnątrz komponentu2. Są one przekazywane tylko w jedną stronę (z góry w dół) i po wyrenderowaniu nie ma możliwości ich zmiany.
Zmiany mogą być realizowane poprzez użycie state (stan). Weźmy prosty przykład, gdy stan zawiera tylko jedną zmienną: licznik. Dodajmy też funkcję zwiększania liznika: count:
class App extends Component {
constructor() {
super();
this.state = {
counter: 0
};
}
count = () => {
this.state.counter=this.state.counter+1; //!!!! to nie jest poprawne !!!
}
render() {
return (
<div>
Licznik: {this.state.counter}
</div>
);
}
}
To rozwiązanie nie jest poprawne! Każda zmiana stanu poinna polegać na wyliczeniu nowego stanu (state), a nie zmiany jego części (counter). Nowy stan ustawia się funkcją setState, która równocześnie powoduje ponowne renderowanie (wyświetlenie) całego obiektu.
Przykład poprawiony [012]:
import React, { Component } from 'react';
import { render } from 'react-dom';
class App extends Component {
constructor() {
super();
this.state = {
counter: 0
};
this.timerId = setInterval(this.increment, 1000);
// Uwaga! Brak zatrzymania zegara!
// zobacz przykład 019
}
increment= () => {
//!!!NIE this.counter=this.counter+1;
let c=this.state.counter+1
this.setState({counter: c});
}
render() {
return (
<div>
Licznik: {this.state.counter}
</div>
);
}
}
render(<App />, document.getElementById('root'));
Do konstruktora dodaliśmy timer (setInterval) co sekundę zmianiający stan (wywołujący metodę increment()
). Co by się jednak stało, gdyby ustawić ten timer w funkcje render()
? Po zmianie stanu (setState) następuje ponowne renderowanie - i pojawiłby się nowy timer. Coraz więcej timerów i coraz szybciej chodzący licznik. Spróbuj! (Zobacz też koniecznie opis cyklu życia i przykład 019).
Powyższy przykład wymaga jeszcze kilka uwag objaśniających:
- zwróć uwagę na wywołanie
super()
- powinno być na początku konstuktora, aby zapewnić wywołanie konsturktora przodka przed dokonaniem innych zmian - stan zawsze inicjujemy w konstruktorze - dlatego elementy ze stanem muszą być definiowane jako klasy, a nie jako funkcje (zob. wstęp).
increment
jest zadeklarowana w postaci funkcji strzałkowych - dzięki temu możemy bez przeszkód odwoływać się do komponentu poprzez this.
Obsługa zdarzeń
React pozwala na definiowanie własnej obsługi zdarzeń - na przykład klinięcia w przycisk. Zamiast timera - wprowadźmy w powyższym przykładzie funkcję click():
click = () => {
let c=this.state.counter+1
this.setState({counter: c});
}
Wywołać tą funkcję można na przykład w obsłudze zdarzenia onClick przycisku. Podobnie jak w przypadku zmiennych i wyrażeń - definicje wywołanie funkcji umieszczamy w nawiasach klamrowych. A oto kompletny przykład [013]:
import React, { Component } from 'react';
import { render } from 'react-dom';
class App extends Component {
constructor() {
super();
this.state = {
counter: 0
};
}
click = () => {
let newc=this.state.counter+1
this.setState({counter: newc});
}
render() {
return (
<div>
Licznik: {this.state.counter}<br />
<button onClick={ this.click }>
Kliknij by zwiększyć
</button>
</div>
);
}
}
render(<App />, document.getElementById('root'));
Jeśli click() zadeklarujemy jako zwykłą funkcję ( a nie funkcję strzałkową) - musimy w definicji obsługi zdarzenia użyć .bind(this)
. Alternatywnie można w obsłudze zdarzenia zdefiniować funkcję strzałkową:
<button onClick={ ()=>this.click() }>
Zobacz przykład [014]:
click() {
let newc=this.state.counter+1
this.setState({counter: newc});
}
render() {
return (
<div>
Licznik: {this.state.counter}<br />
<button onClick={ this.click.bind(this) }>
Kliknij by zwiększyć
</button><br />
// alternatywa:
<button onClick={ ()=>this.click()}>
Kliknij by zwiększyć
</button>
</div>
);
}
Funkcja click() w powyższym przykładzie kryje pewien problem, Kod może nie zadziałać poprawnie, jeśli ktoś zdąży 2 razy kliknąć zanim React przetworzy operację setState (co może nastąpić asynchronicznie). Zobacz: https://facebook.github.io/react/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous
Dlatego zaleca się zamiast podawania nowej, wyliczonej wartości, zdefiniować funkcję wyliczającą tą wartość:
click() {
this.setState(
(prevState, props) => (
{ counter: prevState.counter + 1 }
)
);
}
Zapis (prevState, props) =>
… to funkcja o parametrach prevState i props (poprzedni stan i własności).
Zobacz przykład [015]
Użycie zewnętrznych komponentów
Zwarta definicja nie wymagająca żadnych złożonych interfejsów sprawia, że integracja z różnymi bibliotekami komponentów jest wyjątkowo łatwa. Wystarczy zainstalować (yarn add ...), zaimportować i używać. Na przykład przetworzoną na potrzeby React popularną bibliotekę Bootstrap (https://github.com/react-bootstrap/react-bootstrap\:
import React, { Component } from 'react';
import { render } from 'react-dom';
import {Button, ButtonGroup } from 'react-bootstrap';
class App extends Component {
constructor() {
super();
this.state = {
counter: 0
};
}
click = () => {
this.setState(
(prevState, props) => (
{ counter: prevState.counter + 1 }
)
);
}
render() {
return (
<div>
<ButtonGroup>
<Button bsStyle="warning" onClick={ this.click } >
Kliknij {this.state.counter }
</Button>
</ButtonGroup>
</div>
);
}
}
render(<App />, document.getElementById('root'));
zobacz przyykład [016]
Przekazywanie stanu i obsługi zdarzeń
Stan komponentu może zostać przekazany do użytych w nim (w renderowaniu) komponentów, w których staje się ten stan własnością (props). W taki sam sposó może zostać przekazana obsług zdarzeń (zdarzenia zainicjowane w potomku są obsługiwane w przodku).
Gdy chcemy do komponentu przekazać kod, który ma wywołać się w konkretnych okolicznościach (np. po kliknięciu w przycisk), nie może to być wynik funkcji:
<button onClick={ this.click() } //!Źle
/>
Pamiętajmy, że w trakcie renderowania przy takim zapisie zostanie wywołana funkcja click i jej wynik potraktowany jako funkcja osbługi zdarzenia!
Poprawne zapisy:
<button onClick={ this.click }/>
<button onClick={ () => {this.click()} }/>
W pierwszym przypadku mamy wprost funkcję click. W drugim - nową funkcj anonimową, wywołującą click.
Odrębną kwestią jest zapewnienie, by w funkcji odwołanie do obiektu poprzez this było prawidłowe. Możemy to zapewnić albo poprzez konsekwentne stosowanie funkcji strzałkowych, albo wywołanie bind(this).
Poniższy przykład pokazuje obie te możliwości [017]:
import React, { Component } from 'react';
import { render } from 'react-dom';
class MyButton1 extends Component {
render() {
return <button onClick={this.props.click}>
kliknij[ {this.props.variant}] {this.props.counter}</button>
}
}
class App extends Component {
constructor() {
super();
this.state = {
counter: 0
};
}
click1 = () => {
this.setState(
(prevState, props) => (
{ counter: prevState.counter + 1 }
)
);
}
click2(){
this.setState(
(prevState, props) => (
{ counter: prevState.counter + 1 }
)
);
}
wariantA(){
return 'A';
}
render() {
return (
<div>
<MyButton1
counter={this.state.counter}
//!Źle click={ this.click1() }
click={ this.click1 }
variant={ this.wariantA() }
/>
<MyButton1
counter={this.state.counter}
click={ () => { this.click2() } }
variant='B'
/>
<MyButton1
counter={this.state.counter}
click={ this.click2.bind(this) }
variant='C'
/>
</div>
);
}
}
render(<App />, document.getElementById('root'));
Domyślne własności
Na przykład mamy prosty komponent, który renderuje imię i wiek osoby.
class Person extends React.Component {
render() {
return ( <li> { this.props.name }
<span> (age: { this.props.age }) </span></li> );
}
}
W naszym przypadku oczekujemy dwóch własności:name
iage
.Jeśli chcemy, abyage
opcjonalny i domyślny był tekst "nieznany", możemy skorzystać z domyślnych własności Reacta. Jest to własność defaultProps komponentów.
Na przykład:
class App extends Component {
render() {
Person.defaultProps = { age: 'unknown' };
return (
<ul>
<Person name="Zbigniew Nowak" age="34" />
<Person name="Jan Kowalski" />
</ul>
);
}
}
Zobacz przykład [018]