Praktyczny przykład

Kontynuujemy nasz przykład - grę w kółko i krzyżyk - uzupełniając ją o interakcję z użytkownikiem.

Ponowne uruchomienie serwera deweloperrskiego (localhost:3000):

yarn run start

Dynamika zmian - state

Własności (props) nie są zmieniane inaczej niż w trakcie renderowania strony. Zmiany (ponowne renderowanie) jest inicjowane poprzez zmianę stanu - pola state. Wartości z tego pola mogą być użyte w renderowaniu.

Dodajemy zatem state, zakładając, że zmiany nastąpią po kliknięciu (zdarzenie onClick):

import React from 'react';

class XO extends React.Component {

  constructor(props) {
    super(props);
    this.state = {
      content: '?'
    };
  }

  onClick() {
    console.log('zmiana');
    alert('tu będzie zmiana');
  }

  render() {
    return <td onClick={this.onClick}>{this.state.content}</td>
  }
}

export default XO;

Inicjujemy stan (state) w konstruktorze - wstawiając jedno pole content.

Widzimy, że zastosowano tu konwencję znaną z różnych implementacji szablonów HTML.

Implementujemy funkcję „zmiana” - która ma zmieniać stan naszej aplikacji.

1) Definicja:

zmiana(xo) {
  if (xo==' ') return 'x';
  else if (xo=='x') return 'o';
  else return ' ';
}

2) Użycie: w miejsce „alert” - zdefiniowanie funkcji zmiany z wykorzystaniem zdefiniowanej funkcji..

this.setState( // spowoduje ponowny render()
  (prevState, props) => ({
     content: this.zmiana(prevState.content)
}));

Warto zwrócić uwagę na to, że w miejsce parametru podajemy funkcję, która zwraca nowy stan. To częsta konstrukcja w React (i generalnie w JavaScript) - użycie definicji funkcji tam gdzie w tradycyjnym programowaniu pojawia się odwołanie do obiektu. Jest to związane z asynchronicznością przetwarzania. W ten sposób ponowne klliknięcie w momencie, gdy obsługa poprzedniego nie została zakończona niczym nam nie grozi (zostanie uruchomiona nowa instancja funkcji).

3) Powiązanie funkcji onClick z kontekstem jej wykonania. Po modyfikacjach w funkcji odwołujemy się do zmiennej obiektowej this. To wymaga przekazania (bind) zmiennej dla właściwego obiektu. Możemy we własności – ale lepiej w konstruktorze. Obiektu (dodajemy na końcu konstruktora)

this.onClick = this.onClick.bind(this);

Bez tego pojawi się TypeError: Cannot read property 'setState' of undefined. Chyba, że będziemy konsekwentie używać funkcji strzałkowych.

Uwaga!

Zmieniamy tylko state a nie props. Jeśli w wartości właściwości abc ustawimy {this.state.element}, to props.abc będzie miało wartość taką jak this.state.element.

Zobacz przykład: [xo1]

Stan wspólny i przekazywanie zmian

Mamy wyświetloną planszę i możliwość jej zmian kliknięciem. Jednak te zmiany odbywają się w obrębie komponentu - komórki (XO). Logika gry wymaga aby można było przy ustalaniu następnego stanu wziąć pod uwagę stan wszystkich komórek. Aby tego dokonać - musimy stworzyć stan wspólny - na przykład dla całej planszy. Siłą rzeczy obsługa kliknięć musi zostać przekazana do przodka (planszy).

Siłą rzeczy - nasz element XO zostaje uproszczony - ma tylko wyświetlić stan i wywołać w razie kliknięcia obsługę przekazaną przez przodka:

import React  from 'react';

export class Wynik extends React.Component {
    render() {
        return <span>{this.props.msg}</span>
    }
}

export default class XO extends React.Component {
    zmiana = () => {
     this.props.zmiana(this.props.x,this.props.y)
    }
    render() {
      return <td onClick={this.zmiana}>{this.props.c}</td>;
    }
}

Cała logika aplikacji znajdzie się w pliku App.js:

import React  from 'react';
import './App.css';
import XO, {Wynik} from './xo.js';

export default class App extends React.Component {

/*  const state : {
    xo : [], // znak x / o
    kto : '',  // czyj ruch
    stanGry: ''// komunikat
  };
*/
  constructor(props) {
    super(props);
    this.zmiana=this.zmiana.bind(this);
    this.state = {
      xo: new Array(9).fill('?'), // znak x / o
      kto: 'x', // czyj ruch
      stanGry: '* Kółko i Krzyżyk *' // komunikat
    };
  }

  zmiana(x,y) {
    let ix = y*3 + x;
    if (this.state.xo[ix] !== '?') return;
    let nxo  = this.state.xo.slice(); // PŁYTKA KOPIA state.xo.
    nxo[ix] = this.state.kto;
    let nkto='x';
    let nstan = 'ruch: x';
    if (this.state.kto==='x') {
        nkto='o';
        nstan = 'ruch: o';
    }
    if (    ((nxo[0] !== '?') && (nxo[0]===nxo[1]) && (nxo[0]===nxo[2]))
           || ((nxo[3] !== '?') && (nxo[3]===nxo[4]) && (nxo[3]===nxo[5]))
           || ((nxo[6] !== '?') && (nxo[6]===nxo[7]) && (nxo[6]===nxo[8]))
           || ((nxo[0] !== '?') && (nxo[0]===nxo[3]) && (nxo[0]===nxo[6]))
           || ((nxo[1] !== '?') && (nxo[1]===nxo[4]) && (nxo[1]===nxo[7]))
           || ((nxo[2] !== '?') && (nxo[2]===nxo[5]) && (nxo[2]===nxo[8]))
           || ((nxo[0] !== '?') && (nxo[0]===nxo[4]) && (nxo[0]===nxo[8]))
           || ((nxo[2] !== '?') && (nxo[2]===nxo[4]) && (nxo[2]===nxo[6]))  ) {
             nstan = 'KONIEC';
    }
    this.setState({
       ...this.state, // spread operator 
                     // https://redux.js.org/recipes/using-object-spread-operator
        xo : nxo,
        kto : nkto,
        stanGry : nstan
      });
  }

  render() {
        return (
            <table className="plansza">
                <tbody>
                <tr>
                    <XO x={0} y={0} zmiana={this.zmiana} c={this.state.xo[0]} />
                    <XO x={1} y={0} zmiana={this.zmiana} c={this.state.xo[1]} />
                    <XO x={2} y={0} zmiana={this.zmiana} c={this.state.xo[2]} />
                </tr>
                <tr>
                    <XO x={0} y={1} zmiana={this.zmiana} c={this.state.xo[3]} />
                    <XO x={1} y={1} zmiana={this.zmiana} c={this.state.xo[4]} />
                    <XO x={2} y={1} zmiana={this.zmiana} c={this.state.xo[5]} />
                </tr>
                <tr>
                    <XO x={0} y={2} zmiana={this.zmiana} c={this.state.xo[6]} />
                    <XO x={1} y={2} zmiana={this.zmiana} c={this.state.xo[7]} />
                    <XO x={2} y={2} zmiana={this.zmiana} c={this.state.xo[8]} />
                </tr>
                <tr>
                    <td colSpan="3"><Wynik msg={this.state.stanGry}/></td>
                </tr>
                </tbody>
            </table> );
    }
}

Nowy stan wypracowuje funkcja zmiana, która jest wysoływana przez każde kliknięcie. Jak widać dane są przekazywane wyłącznie z góry w dół, a obsługa zdarzeń może przepływać w kierunku odwrotnym.

Zobacz gotowy przykład: [xo2]