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]