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.createElementdo 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:

  1. 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ą).

  2. 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:nameiage.Jeśli chcemy, abyageopcjonalny 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]