We want to hear from you!Take our 2021 Community Survey!
Ta witryna nie jest już aktualizowana.Przejdź do react.dev

Samouczek: Wstęp do Reacta

These docs are old and won’t be updated. Go to react.dev for the new React docs.

The updated Tutorial teaches modern React and includes live examples.

Ten samouczek nie zakłada jakiejkolwiek znajomości Reacta.

Zanim zaczniemy się uczyć

W tym samouczku skupimy się na stworzeniu niewielkiej gry. Być może po tym zdaniu zechcesz go pominąć, bo nie zajmujesz się pisaniem gier - ale mimo wszystko daj mu szansę. Techniki, których nauczysz się w tym poradniku są fundamentami do zbudowania dowolnej aplikacji w Reakcie, a zrozumienie ich zapewni ci dogłębne poznanie tematu.

Uwaga

Ten samouczek jest przeznaczony dla osób, które wolą uczyć się poprzez praktykę. Jeśli wolisz pouczyć się zagadnień od podstaw, zajrzyj do naszego poradnika krok po kroku. Wiedz jednak, że ten samouczek oraz tamten poradnik nawzajem się uzupełniają.

Samouczek podzieliliśmy na kilka części:

Nie musisz wcale przechodzić przez wszystkie części samouczka naraz, żeby wynieść z niego cokolwiek. Spróbuj jednak dojść najdalej jak możesz - nawet jeśli będą to tylko dwa rozdziały.

Co będziemy budować?

Na koniec nasz kod będzie wyglądał tak: Efekt końcowy. Być może w tej chwili kod wygląda bez sensu albo nie rozumiesz jego składni, ale bez obaw! Celem tego samouczka jest właśnie pomóc ci zrozumieć Reacta i jego składnię.

Zanim przystąpisz do dalszej lektury, zachęcamy cię do uruchomienia gry w “kółko i krzyżyk”. Zwróć uwagę, że jedną z funkcjonalności tej aplikacji jest, znajdująca się na prawo od planszy, numerowana lista. Wyświetla ona na bieżąco wszystkie wykonywane w danej rozgrywce ruchy.

Radzimy wypróbować grę w kółko i krzyżyk przed dalszą lekturą samouczka. Jedną z funkcjonalności, która z pewnością rzuci się od razu w oczy, jest numerowana lista na prawo od planszy. Zawiera ona historię wszystkich ruchów wykonanych podczas gry i jest aktualizowana na bieżąco.

Wymagania

Zakładamy, że HTML i JavaScript nie są ci obce, ale nawet jeśli to twoja pierwsza styczność z tymi językami, to samouczek nie powinien przysporzyć ci większego problemu. Zakładamy również, że znasz takie pojęcia z programowania obiektowego jak “funkcja”, “obiekt” czy “tablica”, a także przynajmniej w niewielkim stopniu rozumiesz, czym jest “klasa”.

Jeśli potrzebujesz odświeżyć sobie wiedzę o JavaScripcie, zalecamy lekturę tego poradnika. Zwróć uwagę, że będziemy tu korzystać z niektórych funkcjonalności standardu ES6 - jednej z ostatnich wersji języka JavaScript - takich jak funkcje strzałkowe (ang. arrow functions), klasy oraz wyrażenia let i const. Możesz skorzystać ze środowiska Babel REPL, aby sprawdzić, co powstanie w wyniku transpilowania kodu napisanego w standardzie ES6.

Konfiguracja pod samouczek

Masz do wyboru dwa sposoby pracy z tym samouczkiem: możesz pisać kod w przeglądarce lub skonfigurować lokalne środowisko na swoim komputerze.

Opcja konfiguracyjna nr 1: Pisanie kodu w przeglądarce

To zdecydowanie najszybszy i najłatwiejszy sposób na rozpoczęcie przygody z Reactem!

Na początek otwórz w nowej karcie szablon startowy. Twoim oczom powinna ukazać się pusta plansza do gry w “kółko i krzyżyk” oraz panel z kodem reactowym. To tutaj będziemy edytować nasz kod.

Możesz teraz pominąć opcję nr 2 i przejść do rozdziału pt. “Przegląd”, w którym przedstawiliśmy ogólne informacje o Reakcie.

Opcja konfiguracyjna nr 2: Lokalne środowisko programistyczne

Ten sposób pracy z naszym samouczkiem jest całkowicie opcjonalny i nie jest konieczny do jego ukończenia!


Opcjonalne: Instrukcje na temat pisania kodu w wybranym przez siebie edytorze tekstu

Ta konfiguracja wymaga więcej wysiłku, lecz pozwala na ukończenie samouczka przy użyciu dowolnego edytora. W tym celu:

  1. Sprawdź, czy masz najnowszą wersję Node.js.
  2. Wykonaj instrukcje instalacji “Create React App”, aby zainicjalizować projekt.
npx create-react-app moja-aplikacja
  1. Usuń wszystkie pliki z folderu src/ w nowo utworzonym projekcie

Uwaga:

Nie usuwaj całego folderu src, tylko znajdujące się w nim pliki. W następnym kroku podmienimy domyślne pliki źródłowe przykładami z samouczka.

cd moja-aplikacja
cd src

# Jeśli używasz Maca lub Linuxa, wpisz:
rm -f *

# Jeśli używasz Windowsa:
del *

# A następnie wróć do folderu z projektem
cd ..
  1. W folderze src/ dodaj plik index.css i wstaw do niego ten kod CSS.
  2. W folderze src/ dodaj plik index.js i wstaw do niego ten kod JS.
  3. Wstaw te trzy wiersze na początku pliku index.js znajdującego się w folderze src/:
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';

Teraz, gdy w folderze z projektem wykonasz polecenie npm start, a następnie wpiszesz w przeglądarce adres http://localhost:3000, na ekranie powinna pojawić się pusta plansza do gry w “kółko i krzyżyk”.

Zalecamy wykonanie tych instrukcji. Pozwoli ci to skonfigurować narzędzia do podświetlania składni w wybranym edytorze tekstu.

Pomocy, nie wiem, co robić!

Jeśli w którymś momencie się zgubisz, przejrzyj listę źródeł wsparcia od społeczności. Zwłaszcza Reactiflux Chat jest znany z szybkiego udzielania pomocy. Jeśli mimo wszystko nie otrzymasz odpowiedzi lub nadal masz z czymś problem, zgłoś nam to, a spróbujemy ci jakoś pomóc.

Przegląd

Teraz, gdy już masz wszystko skonfigurowane, czas dowiedzieć się czegoś o Reakcie!

Czym jest React?

React jest deklaratywną, wydajną i elastyczną biblioteką javascriptową do budowania interfejsów użytkownika. Pozwala na tworzenie złożonych UI przy użyciu małych i odizolowanych od siebie kawałków kodu, zwanych “komponentami”.

React rozróżnia kilka rodzajów komponentów, lecz zacznijmy od klas dziedziczących po React.Component:

class ShoppingList extends React.Component {
  render() {
    return (
      <div className="shopping-list">
        <h1>{this.props.name} - lista zakupów</h1>
        <ul>
          <li>Instagram</li>
          <li>WhatsApp</li>
          <li>Oculus</li>
        </ul>
      </div>
    );
  }
}

// Przykład użycia: <ShoppingList name="Marek" />

Do tych nietypowych znaczników XML-owych wrócimy za chwilę. Komponenty pozwalają wytłumaczyć Reactowi, co chcemy ujrzeć na ekranie. Gdy zmienią się nasze dane, React w sposób efektywny zaktualizuje i ponownie wyrenderuje komponenty.

W powyższym przykładzie ShoppingList (pol. lista zakupów) jest reactowym komponentem klasowym. Na wejściu komponent przyjmuje parametry, nazywane “atrybutami” (ang. props; skrót od properties), i przy pomocy metody render zwraca strukturę widoków do wyświetlenia.

Metoda render zwraca opis tego, co zostanie wyświetlone na ekranie. React analizuje ten opis i wyświetla wynik końcowy renderowania. Precyzyjniej rzecz ujmując, metoda render zwraca element reactowy, który jest zwięzłą reprezentacją tego, co zostanie wyświetlone na ekranie. Większość programistów reactowych używa specjalnej składni zwanej “JSX”, która ułatwia pisanie wspomnianych struktur. Składnia <div /> podczas budowania jest przekształcana w kod: React.createElement('div'). Idąc dalej, powyższy przykład jest równoznaczny z:

return React.createElement('div', {className: 'shopping-list'},
  React.createElement('h1', /* ... h1 children ... */),
  React.createElement('ul', /* ... ul children ... */)
);

Zobacz pełną wersję kodu

Jeśli ciekawość nie daje ci spokoju, opis funkcji createElement() znajdziesz w dokumentacji API, aczkolwiek w tym samouczku nie będziemy jej używać. Zamiast tego skorzystamy ze składni JSX.

Składnia JSX ma pełną moc JavaScriptu. Między klamry można wstawić dowolny kod javascriptowy. Każdy element reactowy jest obiektem, który można przypisać do zmiennej i przekazywać dowolnie w swoim programie.

Powyższy komponent ShoppingList wyrenderuje jedynie wbudowane komponenty znane z drzewa DOM, jak <div /> czy <li />. Ale równie dobrze można wyrenderować w nim inny, własny komponent. Na przykład, do całej listy zakupów można odwołać się pisząc <ShoppingList />. Każdy komponent w Reakcie jest enkapsulowany (ang. encapsulated) i może działać niezależnie; pozwala to na budowanie skomplikowanych interfejsów użytkownika przy użyciu prostych komponentów.

Analizowanie szablonu startowego

Jeśli zamierzasz przejść przez ten samouczek w przeglądarce, otwórz ten link w nowej karcie: Szablon startowy. Jeśli zamierzasz pracować lokalnie, zamiast tego otwórz plik src/index.js, znajdujący się w folderze z projektem (jego tworzenie opisywaliśmy w rozdziale pt. “Konfiguracja pod samouczek”).

Wspomniany “szablon startowy” będzie bazą dla naszej aplikacji. Dodaliśmy już do niego style CSS, więc teraz nareszcie można skupić się na nauce Reacta i implementowaniu gry w “kółko i krzyżyk”.

Analizując kod, łatwo zauważyć, że mamy do czynienia z trzema komponentami reactowymi:

  • Square (pole na planszy),
  • Board (plansza),
  • Game (gra).

Komponent Square (pole) renderuje pojedynczy element <button>, a Board (plansza) renderuje 9 takich pól. Z kolei komponent Game (gra) renderuje planszę wypełnioną symbolami zastępczymi, które podmienimy nieco później. Aplikacja w obecnym stanie nie zawiera żadnych interaktywnych komponentów.

Przekazywanie danych przez atrybuty

Na dobry początek, spróbujmy przekazać jakieś dane z komponentu Board do Square.

W trakcie wykonywania poleceń zalecamy pisanie kodu samodzielnie. Pozwoli ci to bardziej skupić się na poszczególnych zagadnieniach, dzięki czemu lepiej je sobie przyswoisz i zrozumiesz.

W metodzie renderSquare komponentu Board przekaż komponentowi Square atrybut o nazwie value (pol. wartość):

class Board extends React.Component {
  renderSquare(i) {
    return <Square value={i} />;  }
}

Teraz zmodyfikuj metodę render komponentu Square tak, by wyświetlała przekazaną wartość. Wystarczy, że zamienisz {/* TODO */} na {this.props.value}:

class Square extends React.Component {
  render() {
    return (
      <button className="square">
        {this.props.value}      </button>
    );
  }
}

Przed zmianą:

React Devtools

Po zmianie: W każdym wyrenderowanym polu powinien wyświetlać się kolejny numer.

React Devtools

Zobacz dotychczasowy kod

Gratulacje! Właśnie udało ci się “przekazać atrybut” z komponentu nadrzędnego Board do komponentu potomnego Square. Przekazywanie atrybutów to jeden ze sposobów na przepływ danych między rodzicem a komponentem potomnym w drzewie.

Tworzenie interaktywnego komponentu

Teraz sprawmy, aby komponent Square wypełniał się literą “X”, kiedy w niego klikniemy. Najpierw dokonaj zmian w elemencie <button /> zwracanym przez Square w metodzie render():

class Square extends React.Component {
  render() {
    return (
      <button className="square" onClick={function() { console.log('kliknięto w przycisk'); }}>        {this.props.value}
      </button>
    );
  }
}

Teraz gdy klikniesz na polu, w konsoli narzędzi programistycznych zobaczysz wiadomość “click”.

Uwaga

Żeby oszczędzić sobie pisania i zapobiec mylącemu zachowaniu this, od teraz obsługę zdarzeń będziemy zapisywać za pomocą funkcji strzałkowych (ang. arrow functions):

class Square extends React.Component {
 render() {
   return (
     <button className="square" onClick={() => console.log('kliknięto w przycisk')}>       {this.props.value}
     </button>
   );
 }
}

Zauważ, że za pomocą onClick={() => console.log('kliknięto w przycisk')} pod atrybutem onClick przekazujemy funkcję. React wywoła ją dopiero po kliknięciu w przycisk. Częstym błędem jest zapominanie o () => i pisanie onClick={console.log('kliknięto w przycisk')}, co powoduje wyświetlenie wiadomości w momencie renderowania komponentu.

W następnym kroku sprawimy, by komponent Square “pamiętał”, że został kliknięty, i wyświetlał literę “X”. Komponenty mogą “pamiętać” o różnych rzeczach dzięki stanowi (ang. state).

Komponenty w Reakcie można wyposażyć w stan, przypisując w konstruktorze odpowiednią wartość do właściwości klasy this.state. Właściwość this.state powinna być traktowana jako prywatna, dostępna tylko dla komponentu, w którym została zdefiniowana. Przypiszmy więc aktualną wartość przycisku do this.state i nadpisujmy ją przy kliknięciu.

Najpierw musimy dodać konstruktor do klasy, aby móc zainicjalizować stan:

class Square extends React.Component {
  constructor(props) {    super(props);    this.state = {      value: null,    };  }
  render() {
    return (
      <button className="square" onClick={() => console.log('kliknięto w przycisk')}>
        {this.props.value}
      </button>
    );
  }
}

Uwaga

W klasach javascriptowych należy zawsze wywoływać metodę super w konstruktorze klasy potomnej. Dlatego wszystkie komponenty reactowe będące klasą, które mają zdefiniowany konstruktor, powinny zaczynać się od wywołania super(props).

Teraz zmienimy kod w metodzie render komponentu Square tak, aby po kliknięciu wyświetlał wartość aktualnego stanu:

  • zamień this.props.value na this.state.value wewnątrz znacznika <button>,
  • zamień procedurę obsługi zdarzenia onClick={...} na onClick={() => this.setState({value: 'X'})},
  • umieść atrybuty className i onClick w osobnych liniach dla lepszej czytelności kodu.

Po wprowadzeniu powyższych zmian, element <button>, zwracany przez komponent Square, powinien wyglądać następująco:

class Square extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      value: null,
    };
  }

  render() {
    return (
      <button
        className="square"        onClick={() => this.setState({value: 'X'})}      >
        {this.state.value}      </button>
    );
  }
}

Wywołując this.setState z wnętrza procedury obsługi zdarzenia onClick w metodzie render, mówimy Reactowi, aby po każdym kliknięciu ponownie renderował element <button>. Po aktualizacji, wartość zmiennej this.state.value będzie równa "X", dlatego też X zostanie wyświetlone na planszy. Kliknięcie na dowolne pole powinno spowodować wyświetlenie w nim litery “X”.

Gdy w komponencie wywołujesz setState, React automatycznie aktualizuje również wszystkie komponenty znajdujące się poniżej w hierarchii.

Zobacz dotychczasowy kod

Narzędzia deweloperskie

Rozszerzenie o nazwie “React Devtools” dla przeglądarek Chrome i Firefox pozwala na zbadanie drzewa komponentów reactowych za pomocą narzędzi deweloperskich wbudowanych w przeglądarkę.

React Devtools

Dzięki temu rozszerzeniu możesz podejrzeć atrybuty i stan dowolnego komponentu w drzewie.

Po zainstalowaniu rozszerzenia wystarczy kliknąć prawym przyciskiem myszy na dowolnym elemencie strony i wybrać z menu “Zbadaj” (ang. “Inspect”). W pasku zakładek, jako dwie ostatnie po prawej, powinny pojawić się zakładki “⚛️ Components” oraz “⚛️ Profiler”. Skorzystaj z “⚛️ Components”, aby zbadać drzewo komponentów.

Jeśli korzystasz z CodePen, potrzebne będą dodatkowe czynności:

  1. Zaloguj się lub zarejestruj i potwierdź swój adres e-mail (wymagane w celach antyspamowych).
  2. Kliknij na przycisk “Fork”.
  3. Kliknij na “Change View” i wybierz “Debug mode”.
  4. W nowej karcie otworzą się narzędzia deweloperskie z dodatkową zakładką “React”.

Dokończenie gry

Zbudowaliśmy solidne fundamenty pod grę w “kółko i krzyżyk”. Zostało nam jeszcze umożliwienie graczom wykonywanie ruchów na przemian (“X” lub “O”), a także określenie sposobu wybierania zwycięzcy.

Wyciąganie stanu w górę

W obecnej wersji aplikacji każdy z komponentów Square decyduje o własnym stanie. Jednak do rozstrzygnięcia gry potrzebny nam będzie jeden wspólny stan, przechowujący wartości dla wszystkich 9 pól na planszy.

Być może przeszło ci przez myśl, że to komponent Board powinien “pytać” każdy z komponentów Square o jego stan. Nawet jeśli jest to możliwe w Reakcie, odradzamy takiego podejścia, ponieważ kod stanie się wtedy bardziej zagmatwany, podatny na błędy i trudny w utrzymaniu. Zamiast tego należy przenieść stan całej planszy do komponentu nadrzędnego - Board. Komponent ten, poprzez atrybuty, będzie mówił każdemu z komponentów Square, co mają wyrenderować - podobnie jak w jednym z poprzednich podrozdziałów, gdzie przekazywaliśmy kolejne numery do komponentów Square.

Aby móc zebrać dane z wielu komponentów potomnych lub umożliwić dwóm potomkom komunikowanie się ze sobą, należy zadeklarować ich wspólny stan w rodzicu. Taki rodzic może wtedy przekazać poszczególne wartości potomkom poprzez atrybuty; dzięki temu potomkowie będą zsynchronizowani zarówno ze sobą nawzajem, jak i z rodzicem.

Wynoszenie stanu w górę struktury to dość częsty zabieg podczas refaktoryzacji (ang. refactoring) kodu. Wykorzystajmy zatem okazję do wypróbowania tego schematu.

Dodaj konstruktor do komponentu Board i ustaw w nim stan przechowujący tablicę dziewięciu wartości null. Każdy z elementów tej tablicy będzie odpowiadał jednemu polu na planszy:

class Board extends React.Component {
  constructor(props) {    super(props);    this.state = {      squares: Array(9).fill(null),    };  }
  renderSquare(i) {
    return <Square value={i} />;
  }

Gdy później wypełnimy planszę wartościami, tablica this.state.squares będzie wyglądała mniej więcej tak:

[
  'O', null, 'X',
  'X', 'X', 'O',
  'O', null, null,
]

Aktualnie metoda renderSquare komponentu Board wygląda następująco:

  renderSquare(i) {
    return <Square value={i} />;
  }

Na początku tego samouczka przekazaliśmy atrybut value z komponentu Board, aby wyświetlić liczby od 0 do 8 w kolejnych polach planszy. W innym rozdziale zastąpiliśmy liczby literą “X”, określaną na podstawie własnego stanu komponentu Square. To właśnie dlatego Square obecnie ignoruje atrybut value przekazywany z nadrzędnego komponentu Board.

Teraz ponownie zastosujemy mechanizm przekazywania atrybutów. Zmienimy kod komponentu Board tak, aby mówił poszczególnym komponentom Square, jaką mają wartość ("X", "O" lub null). Zdefiniowaliśmy już tablicę squares (pol. pola) w konstruktorze komponentu Board, więc teraz zostaje nam zmodyfikować metodę renderSquare (pol. renderuj pole) tak, aby odczytywała z niej wartości:

  renderSquare(i) {
    return <Square value={this.state.squares[i]} />;  }

Zobacz dotychczasowy kod

Każde pole otrzyma poprzez atrybut value odpowiednio: "X", "O" lub null (w przypadku pola pustego).

Następnie musimy zmienić zachowanie komponentu Square po kliknięciu na nim, ponieważ teraz to Board decyduje, które pola są wypełnione. Musimy zatem określić sposób, w jaki komponent Square może aktualizować stan swojego rodzica. Jako że stan komponentu Board należy tylko do niego, nie możemy tak po prostu nadpisać jego wartości z poziomu potomka Square.

Zamiast tego, do komponentu Square przekażemy odpowiednią funkcję za pomocą atrybutów i sprawimy, by Square wywoływał ją za każdym razem, gdy ktoś kliknie na pole. Zmieńmy więc metodę renderSquare w następujący sposób:

  renderSquare(i) {
    return (
      <Square
        value={this.state.squares[i]}
        onClick={() => this.handleClick(i)}      />
    );
  }

Uwaga

Podzieliliśmy kod zwracanego elementu na kilka linii, aby poprawić czytelność. Dodaliśmy również nawiasy po to, aby JavaScript niepotrzebnie nie wstawił średnika za instrukcją return, powodując tym samym błąd.

Przekazujemy teraz z komponentu Board do Square dwa atrybuty: value i onClick. Atrybut onClick jest funkcją, którą Square może wywołać po kliknięciu. Żeby tak się stało, należy:

  • zamienić this.state.value na this.props.value w metodzie render komponentu Square,
  • zamienić this.setState() na this.props.onClick() w metodzie render komponentu Square,
  • usunąć konstruktor z klasy Square, ponieważ nie będzie ona już przechowywała stanu gry.

Po wprowadzeniu powyższych zmian, kod komponentu Square powinien wyglądać następująco:

class Square extends React.Component {  render() {    return (
      <button
        className="square"
        onClick={() => this.props.onClick()}      >
        {this.props.value}      </button>
    );
  }
}

Gdy użytkownik kliknie na pole, zostanie wywołana funkcja onClick dostarczona przez komponent Board. Dzieje się to dlatego, że:

  1. Atrybut onClick wbudowanego komponentu DOM <button> informuje Reacta, żeby zaczął nasłuchiwać kliknięć użytkownika.
  2. Gdy użytkownik kliknie na przycisk, React wywoła procedurę obsługi zdarzenia onClick zdefiniowaną w metodzie render() komponentu Square.
  3. Procedura ta wywoła funkcję this.props.onClick(), czyli atrybut przekazany przez komponent Board.
  4. Ponieważ komponent Board przekazał swojemu potomkowi atrybut onClick={() => this.handleClick(i)}, kliknięcie w Square spowoduje w konsekwencji wywołanie handleClick(i) wewnątrz komponentu Board.
  5. Nie zdefiniowaliśmy jeszcze metody handleClick(), dlatego aplikacja w tym momencie przestanie działać. Jeśli klikniesz na dowolne pole, zobaczysz błąd na czerwonym tle, mówiący coś w stylu: “this.handleClick is not a function” (pol. this.handleClick nie jest funkcją).

Uwaga

Atrybut onClick elementu DOM <button> ma dla Reacta specjalne znaczenie, ponieważ jest to wbudowany komponent. W przypadku własnych komponentów, jak Square, nazwa tego atrybutu może być dowolna. Moglibyśmy nazwać inaczej zarówno atrybut onClick w Square, jak i handleClick w Board, a kod nadal działałby w taki sam sposób. Przyjęło się jednak określać atrybuty odpowiedzialne za wywołanie zdarzenia jako on[Event], a procedury obsługi zdarzeń jako handle[Event].

Jeśli teraz klikniemy na pole planszy, otrzymamy błąd, ponieważ nie zdefiniowaliśmy jeszcze metody handleClick. Dodajmy ją zatem:

class Board extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      squares: Array(9).fill(null),
    };
  }

  handleClick(i) {    const squares = this.state.squares.slice();    squares[i] = 'X';    this.setState({squares: squares});  }
  renderSquare(i) {
    return (
      <Square
        value={this.state.squares[i]}
        onClick={() => this.handleClick(i)}
      />
    );
  }

  render() {
    const status = 'Następny gracz: X';

    return (
      <div>
        <div className="status">{status}</div>
        <div className="board-row">
          {this.renderSquare(0)}
          {this.renderSquare(1)}
          {this.renderSquare(2)}
        </div>
        <div className="board-row">
          {this.renderSquare(3)}
          {this.renderSquare(4)}
          {this.renderSquare(5)}
        </div>
        <div className="board-row">
          {this.renderSquare(6)}
          {this.renderSquare(7)}
          {this.renderSquare(8)}
        </div>
      </div>
    );
  }
}

Zobacz dotychczasowy kod

Po zastosowaniu powyższych zmian znów będziemy mogli klikać w pola planszy. Różnica polega na tym, że teraz stan planszy jest przechowywany w komponencie Board, nie zaś w każdym z komponentów Square z osobna. Gdy stan w Board ulegnie zmianie, wszystkie pola zostaną ponownie wyrenderowane. Przechowywanie stanu wszystkich pól w jednym miejscu pozwoli nam w przyszłości wyłonić zwycięzcę rozgrywki.

Przenieśliśmy stan z komponentu Square do Board, przez co każdy Square otrzymuje od rodzica swoją wartość i informuje go o kliknięciu. W terminologii reactowej komponenty Square są teraz komponentami kontrolowanymi. Board ma nad nimi pełną kontrolę.

Zauważ, że w metodzie handleClick używamy .slice() do stworzenia kopii tablicy squares, zamiast ją modyfikować bezpośrednio. W następnym podrozdziale dowiesz się dlaczego.

Dlaczego niezmienność jest istotna

W poprzednim przykładzie zasugerowaliśmy stworzenie kopii tablicy squares przy użyciu metody .slice(), zamiast ją bezpośrednio modyfikować. Omówimy teraz bardzo ważne zagadnienie, jakim jest niezmienność (ang. immutability).

Ogólnie rzecz biorąc, istnieją dwa sposoby na zmianę danych. Pierwszym z nich jest mutowanie (ang. mutate) danych poprzez bezpośrednią zmianę ich wartości. Drugim sposobem jest zastąpienie danych ich nową kopią, zawierającą wszystkie potrzebne zmiany.

Zmiana danych poprzez mutację

var player = {score: 1, name: 'Jeff'};
player.score = 2;
// Teraz zmienna "player" ma wartość: {score: 2, name: 'Jeff'}

Zmiana danych bez mutacji

var player = {score: 1, name: 'Jeff'};

var newPlayer = Object.assign({}, player, {score: 2});
// Zmienna "player" nie uległa zmianie, a "newPlayer" jest nowym obiektem o wartościach {score: 2, name: 'Jeff'}

// Jeśli używasz składni "operatora rozszczepienia" obiektu (ang. spread operator), możesz napisać:
// var newPlayer = {...player, score: 2};

Rezultat jest podobny, lecz jeśli zdecydujemy się nie mutować (ani nie zmieniać znajdujących się wewnątrz) danych bezpośrednio, zyskamy kilka korzyści opisanych poniżej.

Skomplikowane funkcjonalności stają się proste

Niezmienność (ang. immutability) sprawia, że skomplikowane funkcjonalności są łatwiejsze w implementacji. W późniejszej części tego samouczka dodamy funkcjonalność “podróży w czasie”, która pozwoli nam przejrzeć historię ruchów gry w “kółko i krzyżyk” i “cofać się w czasie” do wybranego ruchu. Nie jest to jednak coś, co można zaimplementować tylko w grze - możliwość cofania i ponawiania czynności jest dość popularnym wymogiem w aplikacjach. Unikanie bezpośredniej modyfikacji danych pozwoli nam przechowywać poprzednie stany gry nienaruszone i ponownie ich używać.

Wykrywanie zmian

Wykrywanie zmian w mutowalnych (ang. mutable) obiektach jest trudne, ponieważ są one zmieniane bezpośrednio. Tego typu mechanizm detekcji zmian musiałby porównać aktualną i poprzednią wersję obiektu na każdym poziomie jego struktury.

Wykrywanie zmian w niezmiennych (ang. immutable) obiektach jest stosunkowo łatwe. Jeśli referencja aktualnego obiektu jest różna od referencji poprzedniego, to znaczy, że nastąpiła jakaś zmiana.

Kiedy ponownie renderować w Reakcie

Główną korzyścią z niezmienności danych jest możliwość tworzenia w Reakcie tzw. czystych komponentów (ang. pure components). Taki komponent w łatwy sposób może sprawdzić, czy nastąpiły jakiekolwiek zmiany i czy w związku z tym powinien się ponownie wyrenderować.

Aby dowiedzieć się więcej na temat metody shouldComponentUpdate() i budowania czystych komponentów, zajrzyj do rozdziału pt. “Optymalizacja wydajności”.

Komponenty funkcyjne

Przekształcimy teraz Square w komponent funkcyjny (ang. function component).

W Reakcie komponenty funkcyjne są prostym sposobem na stworzenie komponentów posiadających tylko metodę render i niemających żadnego stanu. Zamiast definiować klasę dziedziczącą po React.Component, możemy napisać funkcję z argumentem props, która zwraca strukturę do wyrenderowania. Komponenty funkcyjne są prostsze i czytelniejsze od klas, a ponadto mają szerokie zastosowanie.

Zastąp klasę Square poniższą funkcją:

function Square(props) {
  return (
    <button className="square" onClick={props.onClick}>
      {props.value}
    </button>
  );
}

zmieniając przy okazji wszystkie this.props na props.

Zobacz dotychczasowy kod

Uwaga

Po przekształceniu Square na komponent funkcyjny, zmieniliśmy także onClick={() => this.props.onClick()} na krótsze onClick={props.onClick} (zwróć uwagę na brak nawiasów okrągłych w obu miejscach). W klasie użyliśmy funkcji strzałkowej (ang. arrow function), aby odwołać się do właściwego this, natomiast w komponencie funkcyjnym nie musimy w ogóle martwić się o this.

Granie na zmianę

W następnej kolejności należałoby naprawić widoczny na pierwszy rzut oka defekt: żaden z graczy nie może umieścić na planszy symbolu kółka (“O”).

Ustalmy, że domyślnie “X” wykonuje pierwszy ruch. Aby to zrobić, zmieńmy stan początkowy w konstruktorze komponentu Board:

class Board extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      squares: Array(9).fill(null),
      xIsNext: true,    };
  }

Za każdym razem, gdy użytkownik wykona ruch, zmienna xIsNext (typu zerojedynkowego; ang. boolean) zmieni wartość na przeciwną, informując, który z graczy jest następny w kolejce, a zaraz po tym stan gry zostanie zapisany. Zmieńmy metodę handleClick komponentu Board tak, aby zmieniała flagę xIsNext na przeciwną:

  handleClick(i) {
    const squares = this.state.squares.slice();
    squares[i] = this.state.xIsNext ? 'X' : 'O';    this.setState({
      squares: squares,
      xIsNext: !this.state.xIsNext,    });
  }

Po tej modyfikacji “krzyżyki” i “kółka” będą wykonywać ruchy na zmianę. Spróbuj zagrać!

Zaktualizujmy teraz wartość status w metodzie render, aby poprawnie wyświetlała, który gracz jest następny:

  render() {
    const status = 'Następny gracz: ' + (this.state.xIsNext ? 'X' : 'O');
    return (
      // reszta kodu pozostaje bez zmian

Zmieniony komponent Board powinien wyglądać następująco:

class Board extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      squares: Array(9).fill(null),
      xIsNext: true,    };
  }

  handleClick(i) {
    const squares = this.state.squares.slice();    squares[i] = this.state.xIsNext ? 'X' : 'O';    this.setState({      squares: squares,      xIsNext: !this.state.xIsNext,    });  }

  renderSquare(i) {
    return (
      <Square
        value={this.state.squares[i]}
        onClick={() => this.handleClick(i)}
      />
    );
  }

  render() {
    const status = 'Następny gracz: ' + (this.state.xIsNext ? 'X' : 'O');
    return (
      <div>
        <div className="status">{status}</div>
        <div className="board-row">
          {this.renderSquare(0)}
          {this.renderSquare(1)}
          {this.renderSquare(2)}
        </div>
        <div className="board-row">
          {this.renderSquare(3)}
          {this.renderSquare(4)}
          {this.renderSquare(5)}
        </div>
        <div className="board-row">
          {this.renderSquare(6)}
          {this.renderSquare(7)}
          {this.renderSquare(8)}
        </div>
      </div>
    );
  }
}

Zobacz dotychczasowy kod

Ogłaszanie zwycięzcy

Teraz gdy już wyświetlamy, który z graczy będzie wykonywał następny ruch, powinniśmy również ogłaszać zwycięzcę lub wyświetlać informację o braku możliwych ruchów. Do wyłaniania zwycięzcy posłuży nam następująca funkcja pomocnicza, którą należy dodać na końcu pliku:

function calculateWinner(squares) {
  const lines = [
    [0, 1, 2],
    [