Opas: Ristinolla
Tulet rakentamaan pienen ristinolla-pelin tässä oppaassa. Tämä opas ei oleta aikaisempaa React-osaamista. Tekniikat, joita opit oppaan aikana ovat perustavanlaatuisia mille tahansa React-sovellukselle ja niiden ymmärtäminen antaa sinulle syvällisen ymmärryksen Reactista.
Tämä opas on jaettu useaan osaan:
- Oppaan asennusvaihe antaa sinulle lähtökohdan* oppaan seuraamiseen.
- Yleiskatsaus opettaa sinulle Reactin perusteet: komponentit, propsit, ja tilan.
- Pelin viimeistely opettaa sinulle yleisimmät tekniikat React kehityksessä.
- Aikamatkustuksen lisääminen opettaa sinulle syvällisen ymmärryksen Reactin uniikkeihin vahvuuksiin.
Mitä olet rakentamassa?
Tässä oppaassa tulet rakentamaan interaktiivisen ristinolla-pelin Reactilla.
Näet alla miltä se tulee lopulta näyttämään kun saat sen valmiiksi:
import { useState } from 'react'; function Square({ value, onSquareClick }) { return ( <button className="square" onClick={onSquareClick}> {value} </button> ); } function Board({ xIsNext, squares, onPlay }) { function handleClick(i) { if (calculateWinner(squares) || squares[i]) { return; } const nextSquares = squares.slice(); if (xIsNext) { nextSquares[i] = 'X'; } else { nextSquares[i] = 'O'; } onPlay(nextSquares); } const winner = calculateWinner(squares); let status; if (winner) { status = 'Winner: ' + winner; } else { status = 'Next player: ' + (xIsNext ? 'X' : 'O'); } return ( <> <div className="status">{status}</div> <div className="board-row"> <Square value={squares[0]} onSquareClick={() => handleClick(0)} /> <Square value={squares[1]} onSquareClick={() => handleClick(1)} /> <Square value={squares[2]} onSquareClick={() => handleClick(2)} /> </div> <div className="board-row"> <Square value={squares[3]} onSquareClick={() => handleClick(3)} /> <Square value={squares[4]} onSquareClick={() => handleClick(4)} /> <Square value={squares[5]} onSquareClick={() => handleClick(5)} /> </div> <div className="board-row"> <Square value={squares[6]} onSquareClick={() => handleClick(6)} /> <Square value={squares[7]} onSquareClick={() => handleClick(7)} /> <Square value={squares[8]} onSquareClick={() => handleClick(8)} /> </div> </> ); } export default function Game() { const [history, setHistory] = useState([Array(9).fill(null)]); const [currentMove, setCurrentMove] = useState(0); const xIsNext = currentMove % 2 === 0; const currentSquares = history[currentMove]; function handlePlay(nextSquares) { const nextHistory = [...history.slice(0, currentMove + 1), nextSquares]; setHistory(nextHistory); setCurrentMove(nextHistory.length - 1); } function jumpTo(nextMove) { setCurrentMove(nextMove); } const moves = history.map((squares, move) => { let description; if (move > 0) { description = 'Go to move #' + move; } else { description = 'Go to game start'; } return ( <li key={move}> <button onClick={() => jumpTo(move)}>{description}</button> </li> ); }); return ( <div className="game"> <div className="game-board"> <Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} /> </div> <div className="game-info"> <ol>{moves}</ol> </div> </div> ); } function calculateWinner(squares) { const lines = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6], ]; for (let i = 0; i < lines.length; i++) { const [a, b, c] = lines[i]; if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) { return squares[a]; } } return null; }
Jos et saa selvää koodista vielä taikka koodin syntaksi ei ole tuttua, älä huoli! Tämän oppaan tavoite on auttaa sinua ymmärtämään Reactia ja sen syntaksia.
Suosittelemme, että kokeilet peliä ennen kuin jatkat oppaan kanssa. Yksi pelin ominaisuuksista on, että pelilaudan oikealla puolella on numeroitu lista. Tämä lista näyttää pelin kaikki siirrot ja päivittyy pelin edetessä.
Kun olet pelannut peliä, jatka oppaan kanssa. Tulet aloittamaan yksinkertaisemmasta pohjasta. Seuraava askel on asentaa ympäristö, jotta voit aloittaa pelin rakentamisen.
Oppaan asennusvaihe
Alla olevassa koodieditorissa, paina *Forkkaa oikeassa yläreunassa avataksesi editorin uuteen välilehteen käyttäen CodeSandboxia. CodeSandbox antaa sinun kirjoittaa koodia selaimessasi ja esikatsella miten käyttäjäsi näkevät luomasi sovelluksen. Uuden välilehden tulisi näyttää tyhjä ruutu ja tämän oppaan aloituskoodi.
export default function Square() { return <button className="square">X</button>; }
Yleiskatsaus
Nyt kun olet valmis, annetaan yleiskatsaus Reactista!
Aloituskoodin tarkastelu
CodeSandboxissa näet kolme eri osiota:

- Files osio, jossa on listaus tiedostoista kuten
App.js
,index.js
,styles.css
ja hakemisto nimeltäänpublic
- Koodieditori, jossa näet valitun tiedoston lähdekoodin
- Selain, jossa näet miltä kirjoittamasi koodi näyttää
App.js
tiedoston tulisi olla valittuna Files osiossa. Tiedoston sisältö koodieditorissa tulisi olla seuraava:
export default function Square() {
return <button className="square">X</button>;
}
Selaimen tulisi näyttää neliö, jossa on X:

Katsotaan nyt aloituskoodin tiedostoja.
App.js
Koodi App.js
tiedostossa luo komponentin. Reactissa komponentti on pala uudelleenkäytettävää koodia, joka edustaa palan käyttöliittymää. Komponentteja käytetään renderöimään, hallitsemaan ja päivittämään sovelluksesi UI elementtejä. Katsotaan komponenttia rivi riviltä nähdäksemme mitä tapahtuu:
export default function Square() {
return <button className="square">X</button>;
}
Ensimmäinen rivi määrittelee funktion nimeltään Square
. export
-JavaScript avainsana tekee funktion saavutettavaksi tämän tiedoston ulkopuolelle. default
avainsana kertoo muille tiedostoille, että tämä on pääfunktio tiedostossasi.
export default function Square() {
return <button className="square">X</button>;
}
Seuraava koodirivi palauttaa painonapin. return
-JavaScript avainsanan tarkoittaa, mitä ikinä sen jälkeen tulee, palautetaan se arvo funktion kutsujalle. <button>
on JSX elementti. JSX elementti on yhdistelmä JavaScript koodia ja HTML tageja, jotka kuvaavat mitä haluaisit näyttää. className="square"
on painikkeen ominaisuus taikka propsi, joka ekertoo CSS:lle miten painike tulisi tyylittää. X
on teksti, joka näytetään painikkeen sisällä, ja </button>
sulkee JSX elementin osoittaen, että mitään seuraavaa sisältöä ei tulisi sijoittaa painikkeen sisälle.
styles.css
Paina tiedostosta nimeltään styles.css
CodeSandboxin Files osiossa. Tämä tiedosto määrittelee React sovelluksesi tyylin. Ensimmäiset kaksi CSS selektoria (*
ja body
) määrittävät suuren osan sovelluksestasi tyyleistä, kun taas .square
selektori määrittää minkä tahansa komponentin tyylin, jossa className
ominaisuus on asetettu square
arvoon. Koodissasi tämä vastaa painiketta Square
komponentissa App.js
tiedostossa.
index.js
Paina tiedostosta nimeltään index.js
CodeSandboxin Files osiossa. Et tule muokkaamaan tätä tiedostoa oppaan aikana, mutta se on silta App.js
tiedostossa luomasi komponentin ja selaimen välillä.
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import './styles.css';
import App from './App';
Rivit 1-5 tuovat kaikki tarvittavat palaset yhteen:
- React
- Reactin kirjasto, jolla se juttelee selaimen kanssa (React DOM)
- komponenttiesi tyylit
- luomasi komponentti
App.js
tiedostossa.
Loput tiedostosta tuo kaikki palaset yhteen ja palauttaa lopputuotteen index.html
tiedostoon public
hakemistossa.
Pelilaudan rakentaminen
Palataan takaisin App.js
tiedostoon. Tämä on missä tulet viettämään lopun oppaan ajasta.
Nykyisillään pelilauta on vain yksi neliö, mutta tarvitset yhdeksän! Voit yrittää vain kopioida ja liittää neliösi tehdäksesi kaksi neliötä näin:
export default function Square() {
return <button className="square">X</button><button className="square">X</button>;
}
Saat tämän virheen:
<>...</>
?React komponenttien täytyy palauttaa yksi JSX elementti, ei useampia vierekkäisiä JSX elementtejä kun kaksi painonappia. Korjataksesi tämän käytä fragmenttejä (<>
ja </>
) käärimään useampia vierekkäisiä JSX elementtejä näin:
export default function Square() {
return (
<>
<button className="square">X</button>
<button className="square">X</button>
</>
);
}
Nyt näet:

Hyvä! Nyt sinun tulee kopioida ja littää muutaman kerran saadaksesi yhdeksän neliötä ja sitten…

Voi ei! Neliöt ovat kaikki yhdessä rivissä eikä ruudukossa kuten tarvitset sen pelilaudalla. Korjataksesi tämän sinun tulee ryhmitellä neliöt riveihin div
elementeillä ja lisätä muutama CSS luokka. Samalla kun teet tämän, annat jokaiselle neliölle numeron varmistaaksesi, että tiedät missä jokainen neliö näytetään.
App.js
tiedostossa, päivitä Square
komponentti näyttämään tältä:
export default function Square() {
return (
<>
<div className="board-row">
<button className="square">1</button>
<button className="square">2</button>
<button className="square">3</button>
</div>
<div className="board-row">
<button className="square">4</button>
<button className="square">5</button>
<button className="square">6</button>
</div>
<div className="board-row">
<button className="square">7</button>
<button className="square">8</button>
<button className="square">9</button>
</div>
</>
);
}
styles.css
tiedostossa määritelty CSS tyylittää divit className
:n board-row
arvolla. Nyt kun olet ryhmitellyt komponenttisi riveihin tyylitetyillä div
elementeillä, sinulla on ristinolla-pelilauta:

Mutta nyt sinulla on ongelma. Komponenttisi Square
ei enää ole neliö. Korjataksesi tämän, muuta nimi Square
komponentille Board
:iksi:
export default function Board() {
//...
}
Tässä kohtaa, koodisi tuli näyttää tämänkaltaiselta:
export default function Board() { return ( <> <div className="board-row"> <button className="square">1</button> <button className="square">2</button> <button className="square">3</button> </div> <div className="board-row"> <button className="square">4</button> <button className="square">5</button> <button className="square">6</button> </div> <div className="board-row"> <button className="square">7</button> <button className="square">8</button> <button className="square">9</button> </div> </> ); }
Datan välittäminen propseilla
Seuraavaksi haluat muuttaa neliön arvon tyhjästä X:ksi kun käyttäjä painaa neliötä. Tällä hetkellä sinun täytyisi kopioida ja liittää koodi, joka päivittää neliön yhdeksän kertaa (kerran jokaiselle neliölle)! Sen sijaan, että kopioisit ja liittäisit, Reactin komponenttiarkkitehtuuri antaa sinun luoda uudelleenkäytettävän komponentin välttääksesi sotkuisen, toistuvan koodin.
Ensiksi, kopioit rivin, joka määrittelee ensimmäisen neliösi (<button className="square">1</button>
) Board
komponentistasi uuteen Square
komponenttiin:
function Square() {
return <button className="square">1</button>;
}
export default function Board() {
// ...
}
Sitten päivität Board
komponentin renderöimään sen Square
komponentin käyttäen JSX syntaksia:
// ...
export default function Board() {
return (
<>
<div className="board-row">
<Square />
<Square />
<Square />
</div>
<div className="board-row">
<Square />
<Square />
<Square />
</div>
<div className="board-row">
<Square />
<Square />
<Square />
</div>
</>
);
}
Huomaa miten toisin kuin selainten div
:it, omat komponenttisi Board
ja Square
täytyy alkaa isolla kirjaimella.
Katsotaanpa:

Voi ei! Menetit numeroidut neliöt, jotka sinulla oli aiemmin. Nyt jokaisessa neliössä lukee “1”. Korjataksesi tämän, käytä propseja välittääksesi arvon, jonka jokaisen neliön tulisi saada vanhemmalta komponentilta (Board
) sen alakomponentille (Square
).
Päivitä Square
komponentti lukemaan value
propsi, jonka välität Board
komponentilta:
function Square({ value }) {
return <button className="square">1</button>;
}
function Square({ value })
kertoo, että Square
komponentille voidaan välittää value
niminen propsi.
Nyt haluat näyttää value
arvon 1
:n sijaan jokaisessa neliössä. Kokeile tehdä se näin:
function Square({ value }) {
return <button className="square">value</button>;
}
Oho, tämä ei ollut mitä halusit:

Halusit renderöidä JavaScript muuttujan nimeltään value
komponentistasi, et sanan “value”. Päästäksesi “takaisin JavaScriptiin” JSX:stä, tarvitset aaltosulkeet. Lisää aaltosulkeet value
:n ympärille JSX:ssä näin:
function Square({ value }) {
return <button className="square">{value}</button>;
}
Toistaiseksi, sinun tulisi nähdä tyhjä pelilauta:

Näin tapahtuu, koska Board
komponentti ei ole välittänyt value
propseja jokaiselle Square
komponentille, jonka se renderöi. Korjataksesi tämän, lisää value
propsi jokaiselle Square
komponentille, jonka Board
komponentti renderöi:
export default function Board() {
return (
<>
<div className="board-row">
<Square value="1" />
<Square value="2" />
<Square value="3" />
</div>
<div className="board-row">
<Square value="4" />
<Square value="5" />
<Square value="6" />
</div>
<div className="board-row">
<Square value="7" />
<Square value="8" />
<Square value="9" />
</div>
</>
);
}
Nyt sinun tulisi nähdä numeroitu ruudukko taas:

Päivitetyn koodisi tulisi näyttää tämänkaltaiselta:
function Square({ value }) { return <button className="square">{value}</button>; } export default function Board() { return ( <> <div className="board-row"> <Square value="1" /> <Square value="2" /> <Square value="3" /> </div> <div className="board-row"> <Square value="4" /> <Square value="5" /> <Square value="6" /> </div> <div className="board-row"> <Square value="7" /> <Square value="8" /> <Square value="9" /> </div> </> ); }
Interaktiivisen komponentin luominen
Täytetään Square
komponentti X
:llä kun klikkaat sitä. Määritä funktio nimeltään handleClick
Square
komponentin sisällä. Sitten, lisää onClick
prosi painonapin JSX elementtiin, joka palautetaan Square
komponentista:
function Square({ value }) {
function handleClick() {
console.log('clicked!');
}
return (
<button
className="square"
onClick={handleClick}
>
{value}
</button>
);
}
Jos painat neliöstä nyt, sinun tulisi nähdä loki, jossa lukee "clicked!"
Console välilehdellä Browser osiossa CodeSandboxissa. Painamalla neliötä useammin kuin kerran, lokiin tulee uusi rivi, jossa lukee "clicked!"
. Toistuvat lokit samalla viestillä eivät luo uusia rivejä lokiin. Sen sijaan, näet kasvavan laskurin ensimmäisen "clicked!"
lokin vieressä.
Seuraavaksi, haluat Square komponentin “muistavat”, että sitä painettiin, ja täyttää sen “X” merkillä. Komponentit käyttävät tilaa muistaakseen asioita.
React tarjoaa erityisen funktion nimeltään useState
, jota voit kutsua komponentistasi, jotta se “muistaa” asioita. Tallennetaan Square
komponentin nykyinen arvo tilaan ja muutetaan sitä, kun Square
painetaan.
Importtaa useState
tiedoston ylläosassa. Poista value
propsi Square
komponentista. Sen sijaan, lisää uusi rivi Square
komponentin alkuun, joka kutsuu useState
:a. Anna sen palauttaa tilamuuttuja nimeltään value
:
import { useState } from 'react';
function Square() {
const [value, setValue] = useState(null);
function handleClick() {
//...
value
pitää sisällään arvon ja setValue
on funktio, jota voidaan käyttää muuttamaan arvoa. null
, joka välitetään useState
:lle, käytetään alkuperäisenä arvona tälle tilamuuttujalle, joten value
on aluksi null
.
Koska Square
komponentti ei enää hyväksy propseja, poistat value
propin kaikista yhdeksästä Square
komponentista, jotka Board
komponentti luo:
// ...
export default function Board() {
return (
<>
<div className="board-row">
<Square />
<Square />
<Square />
</div>
<div className="board-row">
<Square />
<Square />
<Square />
</div>
<div className="board-row">
<Square />
<Square />
<Square />
</div>
</>
);
}
Nyt muutat Square
:n näyttämään “X”:n kun sitä painetaan. Korvaa console.log("clicked!");
tapahtumankäsittelijä setValue('X');
:lla. Nyt Square
komponenttisi näyttää tältä:
function Square() {
const [value, setValue] = useState(null);
function handleClick() {
setValue('X');
}
return (
<button
className="square"
onClick={handleClick}
>
{value}
</button>
);
}
Kutsumalla set
funktiota onClick
käsittelijästä, kerrot Reactille renderöidä Square
:n uudelleen aina kun sen <button>
:ia painetaan. Päivityksen jälkeen, Square
n value
on 'X'
, joten näet “X”:n pelilaudalla. Paina mitä tahansa neliötä, ja “X”:n tulisi näkyä:

Jokaisella Squarella on sen oma tila: value
joka on tallennettu jokaisessa Squaressa on täysin riippumaton muista. Kun kutsut set
funktiota komponentissa, React päivittää automaattisesti myös alakomponentit.
Kun olet tehnyt yllä olevat muutokset, koodisi tulisi näyttää tältä:
import { useState } from 'react'; function Square() { const [value, setValue] = useState(null); function handleClick() { setValue('X'); } return ( <button className="square" onClick={handleClick} > {value} </button> ); } export default function Board() { return ( <> <div className="board-row"> <Square /> <Square /> <Square /> </div> <div className="board-row"> <Square /> <Square /> <Square /> </div> <div className="board-row"> <Square /> <Square /> <Square /> </div> </> ); }
React kehitystyökalut
React kehitystyökalujen avulla voit tarkastella React komponenttiesi propseja ja tilaa. React DevTools välilehti löytyy browser osion alapuolelta CodeSandboxissa:

Tarkastellaksesi tiettyä komponenttia ruudulla, käytä nappia React DevToolsin vasemmassa yläkulmassa:

Pelin viimeistely
Tähän mennessä, sinulla on kaikki peruspalikat ristinolla-peliisi. Saadaksesi täydellisen pelin, sinun täytyy nyt vuorotella “X”:n ja “O”:n laittamista pelilaudalle, ja sinun täytyy keksiä tapa määrittää voittaja.
Tilan nostaminen ylös
Tällä hetkellä, jokainen Square
komponentti ylläpitää osaa pelin tilasta. Voittaaksesi ristinolla-pelin, Board
komponentin täytyy jotenkin tietää jokaisen yhdeksän Square
komponentin tila.
Miten lähestyisit tätä? Aluksi, kuten saatat arvata, Board
:n täytyy “kysyä” jokaiselta Square
:lta sen tila. Vaikka tämä lähestymistapa on teknisesti mahdollista Reactissa, emme suosittele sitä, koska koodista tulee vaikeaa ymmärtää, altistaen se bugeille, ja vaikea refaktoroida. Sen sijaan, paras lähestymistapa on tallentaa pelin tila ylemmässä Board
komponentissa jokaisen Square
komponentin sijaan. Board
komponentti voi kertoa jokaiselle Square
komponentille mitä näyttää välittämällä propseja, kuten teit kun välitit numeron jokaiselle Square
komponentille.
Kerätäksesi dataa useammista alakomponenteista, tai saadaksesi kahden alakomponentin kommunikoimaan toistensa kanssa, määritä jaettu tila niitä ylemmässä komponentissa. Ylempi komponentti voi välittää tilan takaisin alakomponenteilleen propseina. Tämä pitää alakomponentit synkronoituina toistensa ja yläkomponentin kanssa.
Tilan nostaminen yläkomponenttiin on yleistä kun React komponentteja refaktoroidaan.
Otetaan tilaisuus kokeilla tätä. Muokkaa Board
komponenttia siten, että se määrittelee tilamuuttujan nimeltään squares
, joka oletuksena on taulukko, jossa on yhdeksän null
arvoa vastaten yhdeksää neliötä:
// ...
export default function Board() {
const [squares, setSquares] = useState(Array(9).fill(null));
return (
// ...
);
}
Array(9).fill(null)
luo taulukon yhdeksällä kohdalla ja asettaa jokaisen niistä null
arvoon. useState()
kutsu sen ympärillä määrittelee squares
tilamuuttujan, jonka arvo on aluksi asetettu tuohon taulukkoon. Jokainen taulukon kohta vastaa neliön arvoa. Kun täytät pelilaudan myöhemmin, squares
taulukko näyttää tältä:
['O', null, 'X', 'X', 'X', 'O', 'O', null, null]
Nyt Board
komponenttisi täytyy välittää value
propsi jokaiselle Square
komponentille, jonka se renderöi:
export default function Board() {
const [squares, setSquares] = useState(Array(9).fill(null));
return (
<>
<div className="board-row">
<Square value={squares[0]} />
<Square value={squares[1]} />
<Square value={squares[2]} />
</div>
<div className="board-row">
<Square value={squares[3]} />
<Square value={squares[4]} />
<Square value={squares[5]} />
</div>
<div className="board-row">
<Square value={squares[6]} />
<Square value={squares[7]} />
<Square value={squares[8]} />
</div>
</>
);
}
Seuraavakasi, muokkaa Square
komponentti vastaanottamaan value
propsi Board komponentilta. Tämä vaatii Square
komponentin oman tilamuuttujan value
ja painonapin onClick
propsin poistamisen:
function Square({value}) {
return <button className="square">{value}</button>;
}
Tässä kohtaa sinun tulisi nähdä tyhjä ristinolla-pelilauta:

Ja koodisi tulisi näyttää tältä:
import { useState } from 'react'; function Square({ value }) { return <button className="square">{value}</button>; } export default function Board() { const [squares, setSquares] = useState(Array(9).fill(null)); return ( <> <div className="board-row"> <Square value={squares[0]} /> <Square value={squares[1]} /> <Square value={squares[2]} /> </div> <div className="board-row"> <Square value={squares[3]} /> <Square value={squares[4]} /> <Square value={squares[5]} /> </div> <div className="board-row"> <Square value={squares[6]} /> <Square value={squares[7]} /> <Square value={squares[8]} /> </div> </> ); }
Jokainen Square saa nyt value
propsin, joka on joko 'X'
, 'O'
, tai null
tyhjille neliöille.
Seuraavaksi, sinun täytyy muuttaa mitä tapahtuu kun Square
:a klikataan. Board
komponentti nyt ylläpitää mitkä neliöt ovat täytettyjä. Sinun täytyy luoda tapa Square
:lle päivittää Board
:n tila. Koska tila on yksityistä komponentille, joka sen määrittelee, et voi päivittää Board
:n tilaa suoraan Square
:sta.
Sen sijaan, välität funktion Board
komponentista Square
komponentille, ja kutsut sitä Square
:sta kun neliötä painetaan. Aloitat funktiosta, jota Square
komponentti kutsuu kun sitä painetaan. Kutsut sitä onSquareClick
:ssa:
function Square({ value }) {
return (
<button className="square" onClick={onSquareClick}>
{value}
</button>
);
}
Seuraavaksi, lisäät onSquareClick
funktion Square
komponentin propseihin:
function Square({ value, onSquareClick }) {
return (
<button className="square" onClick={onSquareClick}>
{value}
</button>
);
}
Nyt yhdistät onSquareClick
propsin Board
komponentin funktioon, jonka nimeät handleClick
. Yhdistääksesi onSquareClick
handleClick
:iin, välität funktion onSquareClick
propsin ensimmäiselle Square
komponentille:
export default function Board() {
const [squares, setSquares] = useState(Array(9).fill(null));
return (
<>
<div className="board-row">
<Square value={squares[0]} onSquareClick={handleClick} />
//...
);
}
Lopuksi, määrittelet handleClick
funktion Board komponentin sisällä päivittämään squares
taulukon ylläpitämään pelilaudan tilaa:
export default function Board() {
const [squares, setSquares] = useState(Array(9).fill(null));
function handleClick() {
const nextSquares = squares.slice();
nextSquares[0] = "X";
setSquares(nextSquares);
}
return (
// ...
)
}
handleClick
funktio luo kopion squares
taulukosta (nextSquares
) JavaScriptin slice()
taulukkometodilla. Sitten, handleClick
päivittää nextSquares
taulukon lisäämällä X
:n ensimmäiseen ([0]
indeksi) neliöön.
Kutsumalla setSquares
funktiota kerrot Reactille, että komponentin tila on muuttunut. Tämä käynnistää renderöinnin komponenteille, jotka käyttävät squares
tilaa (Board
) sekä sen alakomponenteille (Square
komponentit, jotka muodostavat pelilaudan).
Nyt voit lisätä X:ät pelilaudalle… mutta vain ylävasempaan neliöön. handleClick
funktiosi on kovakoodattu päivittämään ylävasemman neliön indeksiä (0
). Päivitetään handleClick
funktio päivittämään mitä tahansa neliötä. Lisää argumentti i
handleClick
funktioon, joka ottaa neliön indeksin, jota päivittää:
export default function Board() {
const [squares, setSquares] = useState(Array(9).fill(null));
function handleClick(i) {
const nextSquares = squares.slice();
nextSquares[i] = "X";
setSquares(nextSquares);
}
return (
// ...
)
}
Seuraavaksi, sinun täytyy välittää i
handleClick
:lle. Voit yrittää asettaa onSquareClick
propin neliölle suoraan JSX:ssä handleClick(0)
näin, mutta se ei toimi:
<Square value={squares[0]} onSquareClick={handleClick(0)} />
Syy miksi tämä ei toimi on, handleClick(0)
kustu on osa pelilaudan renderöintiä. Koska handleClick(0)
muuttaa pelilaudan tilaa kutsumalla setSquares
:ia, koko pelilauta renderöidään uudelleen. Mutta tämä ajaa handleClick(0)
uudelleen, mikä johtaa loputtomaan silmukkaan:
Miksi tämä ongelma ei tapahtunut aiemmin?
Kun välitit onSquareClick={handleClick}
, välitit handleClick
funktion propseina. Et kutsunut sitä! Mutta nyt kutsut sitä heti—huomaa sulkeet handleClick(0)
—ja siksi se ajetaan liian aikaisin. Et halua kutsua handleClick
ennen kuin käyttäjä klikkaa!
Voisit korjata tämän tekemällä funktion kuten handleFirstSquareClick
, joka kutsuu handleClick(0)
, funktion kuten handleSecondSquareClick
, joka kutsuu handleClick(1)
, ja niin edelleen. Välittäisit (et kutsuisi) näitä funktioita propseina kuten onSquareClick={handleFirstSquareClick}
. Tämä korjaisi loputtoman silmukan.
Yhdeksän eri funktion määritteleminen ja nimeäminen on liian raskasta. Sen sijaan tehdään näin:
export default function Board() {
// ...
return (
<>
<div className="board-row">
<Square value={squares[0]} onSquareClick={() => handleClick(0)} />
// ...
);
}
Huomaa uusi () =>
syntaksi. Tässä, () => handleClick(0)
on nuolifunktio, joka on lyhyempi tapa määritellä funktioita. Kun neliötä painetaan, koodi =>
“nuolen” jälkeen ajetaan, kutsuen handleClick(0)
.
Nyt sinun tulee päivittää muut kahdeksan neliötä kutsumaan handleClick
nuolifunktioista, jotka välität. Varmista, että argumentti jokaiselle handleClick
kutsulle vastaa oikean neliön indeksiä:
export default function Board() {
// ...
return (
<>
<div className="board-row">
<Square value={squares[0]} onSquareClick={() => handleClick(0)} />
<Square value={squares[1]} onSquareClick={() => handleClick(1)} />
<Square value={squares[2]} onSquareClick={() => handleClick(2)} />
</div>
<div className="board-row">
<Square value={squares[3]} onSquareClick={() => handleClick(3)} />
<Square value={squares[4]} onSquareClick={() => handleClick(4)} />
<Square value={squares[5]} onSquareClick={() => handleClick(5)} />
</div>
<div className="board-row">
<Square value={squares[6]} onSquareClick={() => handleClick(6)} />
<Square value={squares[7]} onSquareClick={() => handleClick(7)} />
<Square value={squares[8]} onSquareClick={() => handleClick(8)} />
</div>
</>
);
};
Nyt voit taas lisätä X:ät mihin tahansa neliöön pelilaudalla painamalla niitä:

Mutta tällä kertaa tilanhallinta on Board
komponentin vastuulla!
Tämä on mitä koodisi tulisi näyttää:
import { useState } from 'react'; function Square({ value, onSquareClick }) { return ( <button className="square" onClick={onSquareClick}> {value} </button> ); } export default function Board() { const [squares, setSquares] = useState(Array(9).fill(null)); function handleClick(i) { const nextSquares = squares.slice(); nextSquares[i] = 'X'; setSquares(nextSquares); } return ( <> <div className="board-row"> <Square value={squares[0]} onSquareClick={() => handleClick(0)} /> <Square value={squares[1]} onSquareClick={() => handleClick(1)} /> <Square value={squares[2]} onSquareClick={() => handleClick(2)} /> </div> <div className="board-row"> <Square value={squares[3]} onSquareClick={() => handleClick(3)} /> <Square value={squares[4]} onSquareClick={() => handleClick(4)} /> <Square value={squares[5]} onSquareClick={() => handleClick(5)} /> </div> <div className="board-row"> <Square value={squares[6]} onSquareClick={() => handleClick(6)} /> <Square value={squares[7]} onSquareClick={() => handleClick(7)} /> <Square value={squares[8]} onSquareClick={() => handleClick(8)} /> </div> </> ); }
Nyt kun tilanhallintasi on Board
komponentissa, yläkomponentti Board
välittää propseja alakomponenteille Square
komponenteille, jotta ne voidaan näyttää oikein. Kun neliötä painetaan, alakomponentti Square
kysyy yläkomponentti Board
:lta tilan päivittämistä pelilaudalla. Kun Board
:n tila muuttuu, sekä Board
komponentti että jokainen Square
renderöidään uudelleen automaattisesti. Pitämällä kaikkien neliöiden tila Board
komponentissa, se pystyy määrittämään voittajan tulevaisuudessa.
Käydään läpi mitä tapahtuu kun käyttäjä painaa ylävasenta neliötä pelilaudalla lisätäkseen siihen X
:n:
- Ylävasemman neliön klikkaaminen suorittaa funktion, jonka
button
saionClick
propsinaSquare
komponentilta.Square
komponentti sai funktiononSquareClick
propsinaBoard
komponentilta.Board
komponentti määritteli funktion suoraan JSX:ssä. Se kutsuuhandleClick
funktiota argumentilla0
. handleClick
käyttää argumenttia (0
) päivittääkseensquares
taulukon ensimmäisen elementinnull
arvostaX
arvoon.squares
tilaBoard
komponentissa päivitettiin, jotenBoard
ja kaikki sen alakomponentit renderöitiin uudelleen. Tämä aiheuttaaSquare
komponentinvalue
propin muuttumisen indeksillä0
null
arvostaX
arvoon.
Lopussa käyttäjä näkee, että ylävasen neliö on muuttunut tyhjästä X
:ksi sen painamisen jälkeen.
Miksi muuttumattomuus on tärkeää
Huomaa miten handleClick
:ssa kutsut .slice()
luodaksesi kopion squares
taulukosta sen sijaan, että muuttaisit olemassaolevaa taulukkoa. Selittääksemme miksi, meidän täytyy keskustella muuttumattomuudesta ja miksi muuttumattomuus on tärkeää oppia.
On kaksi yleistä tapaa muuttaa dataa. Ensimmäinen tapa on mutatoida dataa muuttamalla suoraan datan arvoja. Toinen tapa on korvata data uudella kopiolla, jossa on halutut muutokset. Tässä on miltä se näyttäisi, jos mutatoisit squares
taulukkoa:
const squares = [null, null, null, null, null, null, null, null, null];
squares[0] = 'X';
// Nyt`squares` on arvoltaan ["X", null, null, null, null, null, null, null, null];
Ja tässä on miltä se näyttäisi jos muuttaisit dataa mutatoimatta squares
taulukkoa:
const squares = [null, null, null, null, null, null, null, null, null];
const nextSquares = ['X', null, null, null, null, null, null, null, null];
// Nyt `squares` on muttumaton, mutta `nextSquares`:n ensimmäinen solu on 'X' `null`:n sijaan
Lopputulos on sama, mutta mutatoimatta (muuttamatta alla olevaa dataa) suoraan, saat useita etuja.
Muuttumattomuus tekee monimutkaisten ominaisuuksien toteuttamisesta paljon helpompaa. Myöhemmin tässä oppaassa, toteutat “aikamatkustuksen” ominaisuuden, joka antaa sinun tarkastella pelin historiaa ja “hypätä takaisin” menneisiin siirtoihin. Tämä toiminnallisuus ei ole pelien erityispiirre—kyky peruuttaa ja palauttaa tiettyjä toimintoja on yleinen vaatimus sovelluksille. Suoran datan mutaation välttäminen antaa sinun pitää edelliset versiot datasta ehjänä, ja käyttää niitä myöhemmin.
On myös toinen etu muuttumattomuudessa. Oletuksena, kaikki lapsikomponentit renderöidään automaattisesti uudelleen, kun yläkomponentin tila muuttuu. Tämä sisältää jopa lapsikomponentit, jotka eivät olleet vaikuttuneita muutoksesta. Vaikka renderöinti ei ole itsessään huomattavaa käyttäjälle (sinun ei pitäisi aktiivisesti yrittää välttää sitä!), saatat haluta ohittaa renderöinnin puun osalta, joka ei selvästi ollut vaikuttunut siitä suorituskyky syistä. Muuttumattomuus tekee hyvin halvaksi komponenteille verrata onko niiden data muuttunut vai ei. Voit oppia lisää siitä, miten React valitsee milloin renderöidä komponentti uudelleen memo
API referenssistä.
Vuorojen ottaminen
Nyt on aika korjata suuri vika tässä ristinolla-pelissä: “0”:a ei voi merkitä pelilaudalle.
Asetat ensimmäisen siirron oletuksena “X”:ksi. Pidetään kirjaa tästä lisäämällä toinen tilamuuttuja Board
komponenttiin:
function Board() {
const [xIsNext, setXIsNext] = useState(true);
const [squares, setSquares] = useState(Array(9).fill(null));
// ...
}
Joka kerta kun pelaaja siirtää, xIsNext
(totuusarvo) käännetään määrittämään kumpi pelaaja siirtää seuraavaksi ja pelin tila tallennetaan. Päivität Board
komponentin handleClick
funktion kääntämään xIsNext
arvon:
export default function Board() {
const [xIsNext, setXIsNext] = useState(true);
const [squares, setSquares] = useState(Array(9).fill(null));
function handleClick(i) {
const nextSquares = squares.slice();
if (xIsNext) {
nextSquares[i] = "X";
} else {
nextSquares[i] = "O";
}
setSquares(nextSquares);
setXIsNext(!xIsNext);
}
return (
//...
);
}
Nyt kun klikkaat eri neliöitä, ne vaihtelevat X
ja 0
välillä, kuten niiden pitäisi!
Mutta hetkonen, tässä on ongelma. Kokeile klikata samaa neliötä useamman kerran:

X
ylikirjoitetaan 0
:lla! Vaikka tämä lisäisikin mielenkiint0isen käänteen peliin, pysytään alkuperäisissä säännöissä toistaiseksi.
Kun merkitset neliön X
:llä tai 0
:lla, et ensin tarkista onko neliöllä jo X
tai 0
arvoa. Voit korjata tämän palaamalla aikaisin. Tarkistat onko neliöllä jo X
tai 0
arvo. Jos neliö on jo täytetty, return
handleClick
funktiossa aikaisin—ennen kuin se yrittää päivittää pelilaudan tilaa.
function handleClick(i) {
if (squares[i]) {
return;
}
const nextSquares = squares.slice();
//...
}
Nyt voit lisätä vain X
tai 0
tyhjille neliöille! Tässä on mitä koodisi tulisi näyttää tässä vaiheessa:
import { useState } from 'react'; function Square({value, onSquareClick}) { return ( <button className="square" onClick={onSquareClick}> {value} </button> ); } export default function Board() { const [xIsNext, setXIsNext] = useState(true); const [squares, setSquares] = useState(Array(9).fill(null)); function handleClick(i) { if (squares[i]) { return; } const nextSquares = squares.slice(); if (xIsNext) { nextSquares[i] = 'X'; } else { nextSquares[i] = 'O'; } setSquares(nextSquares); setXIsNext(!xIsNext); } return ( <> <div className="board-row"> <Square value={squares[0]} onSquareClick={() => handleClick(0)} /> <Square value={squares[1]} onSquareClick={() => handleClick(1)} /> <Square value={squares[2]} onSquareClick={() => handleClick(2)} /> </div> <div className="board-row"> <Square value={squares[3]} onSquareClick={() => handleClick(3)} /> <Square value={squares[4]} onSquareClick={() => handleClick(4)} /> <Square value={squares[5]} onSquareClick={() => handleClick(5)} /> </div> <div className="board-row"> <Square value={squares[6]} onSquareClick={() => handleClick(6)} /> <Square value={squares[7]} onSquareClick={() => handleClick(7)} /> <Square value={squares[8]} onSquareClick={() => handleClick(8)} /> </div> </> ); }
Voittajan päättäminen
Nyt kun pelaajat voivat ottaa vuoroja, haluat näyttää kun peli on voitettu ja ei ole enää vuoroja tehtävänä. Tämän tekemiseksi lisäät apufunktion nimeltä calculateWinner
, joka ottaa yhdeksän neliön taulukon, tarkistaa onko voittaja ja palauttaa 'X'
, 'O'
, tai null
tarvittaessa. Älä huoli liikaa calculateWinner
funktiosta; se ei ole Reactiin erityinen:
export default function Board() {
//...
}
function calculateWinner(squares) {
const lines = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
[0, 4, 8],
[2, 4, 6]
];
for (let i = 0; i < lines.length; i++) {
const [a, b, c] = lines[i];
if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
return squares[a];
}
}
return null;
}
Kutsut calculateWinner(squares)
Board
komponentin handleClick
funktiossa tarkistaaksesi onko pelaaja voittanut. Voit suorittaa tämän tarkistuksen samaan aikaan kun tarkistat onko käyttäjä klikannut neliötä, jossa on jo X
tai O
. Haluamme palata aikaisin molemmissa tapauksissa:
function handleClick(i) {
if (squares[i] || calculateWinner(squares)) {
return;
}
const nextSquares = squares.slice();
//...
}
Antaaksesi pelaajiesi tietää milloin peli on ohi, voit näyttää tekstin kuten “Winner: X” tai “Winner: 0”. Tämän tekemiseksi lisäät status
osion Board
komponenttiin. Status näyttää voittajan, jos peli on ohi ja jos peli on kesken, näytät kumman pelaajan vuoro on seuraavaksi:
export default function Board() {
// ...
const winner = calculateWinner(squares);
let status;
if (winner) {
status = "Winner: " + winner;
} else {
status = "Next player: " + (xIsNext ? "X" : "O");
}
return (
<>
<div className="status">{status}</div>
<div className="board-row">
// ...
)
}
Onneksi olkoon! Sinulla on nyt toimi ristinolla-peli. Ja olet juuri oppinut Reactin perusteet. Joten sinä olet oikea voittaja tässä. Tässä on miltä koodisi tulisi näyttää:
import { useState } from 'react'; function Square({value, onSquareClick}) { return ( <button className="square" onClick={onSquareClick}> {value} </button> ); } export default function Board() { const [xIsNext, setXIsNext] = useState(true); const [squares, setSquares] = useState(Array(9).fill(null)); function handleClick(i) { if (calculateWinner(squares) || squares[i]) { return; } const nextSquares = squares.slice(); if (xIsNext) { nextSquares[i] = 'X'; } else { nextSquares[i] = 'O'; } setSquares(nextSquares); setXIsNext(!xIsNext); } const winner = calculateWinner(squares); let status; if (winner) { status = 'Winner: ' + winner; } else { status = 'Next player: ' + (xIsNext ? 'X' : 'O'); } return ( <> <div className="status">{status}</div> <div className="board-row"> <Square value={squares[0]} onSquareClick={() => handleClick(0)} /> <Square value={squares[1]} onSquareClick={() => handleClick(1)} /> <Square value={squares[2]} onSquareClick={() => handleClick(2)} /> </div> <div className="board-row"> <Square value={squares[3]} onSquareClick={() => handleClick(3)} /> <Square value={squares[4]} onSquareClick={() => handleClick(4)} /> <Square value={squares[5]} onSquareClick={() => handleClick(5)} /> </div> <div className="board-row"> <Square value={squares[6]} onSquareClick={() => handleClick(6)} /> <Square value={squares[7]} onSquareClick={() => handleClick(7)} /> <Square value={squares[8]} onSquareClick={() => handleClick(8)} /> </div> </> ); } function calculateWinner(squares) { const lines = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6], ]; for (let i = 0; i < lines.length; i++) { const [a, b, c] = lines[i]; if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) { return squares[a]; } } return null; }
Aikamatkustuksen lisääminen
As a final exercise, let’s make it possible to “go back in time” to the previous moves in the game.
Pelin siirtojen tallentaminen
If you mutated the squares
array, implementing time travel would be very difficult.
However, you used slice()
to create a new copy of the squares
array after every move, and treated it as immutable. This will allow you to store every past version of the squares
array, and navigate between the turns that have already happened.
You’ll store the past squares
arrays in another array called history
, which you’ll store as a new state variable. The history
array represents all board states, from the first to the last move, and has a shape like this:
[
// Before first move
[null, null, null, null, null, null, null, null, null],
// After first move
[null, null, null, null, 'X', null, null, null, null],
// After second move
[null, null, null, null, 'X', null, null, null, 'O'],
// ...
]
Tilan nostaminen ylös, uudestaan
You will now write a new top-level component called Game
to display a list of past moves. That’s where you will place the history
state that contains the entire game history.
Placing the history
state into the Game
component will let you remove the squares
state from its child Board
component. Just like you “lifted state up” from the Square
component into the Board
component, you will now lift it up from the Board
into the top-level Game
component. This gives the Game
component full control over the Board
’s data and lets it instruct the Board
to render previous turns from the history
.
First, add a Game
component with export default
. Have it render the Board
component and some markup:
function Board() {
// ...
}
export default function Game() {
return (
<div className="game">
<div className="game-board">
<Board />
</div>
<div className="game-info">
<ol>{/*TODO*/}</ol>
</div>
</div>
);
}
Note that you are removing the export default
keywords before the function Board() {
declaration and adding them before the function Game() {
declaration. This tells your index.js
file to use the Game
component as the top-level component instead of your Board
component. The additional div
s returned by the Game
component are making room for the game information you’ll add to the board later.
Add some state to the Game
component to track which player is next and the history of moves:
export default function Game() {
const [xIsNext, setXIsNext] = useState(true);
const [history, setHistory] = useState([Array(9).fill(null)]);
// ...
Notice how [Array(9).fill(null)]
is an array with a single item, which itself is an array of 9 null
s.
To render the squares for the current move, you’ll want to read the last squares array from the history
. You don’t need useState
for this—you already have enough information to calculate it during rendering:
export default function Game() {
const [xIsNext, setXIsNext] = useState(true);
const [history, setHistory] = useState([Array(9).fill(null)]);
const currentSquares = history[history.length - 1];
// ...
Next, create a handlePlay
function inside the Game
component that will be called by the Board
component to update the game. Pass xIsNext
, currentSquares
and handlePlay
as props to the Board
component:
export default function Game() {
const [xIsNext, setXIsNext] = useState(true);
const [history, setHistory] = useState([Array(9).fill(null)]);
const currentSquares = history[history.length - 1];
function handlePlay(nextSquares) {
// TODO
}
return (
<div className="game">
<div className="game-board">
<Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
//...
)
}
Let’s make the Board
component fully controlled by the props it receives. Change the Board
component to take three props: xIsNext
, squares
, and a new onPlay
function that Board
can call with the updated squares array when a player makes a move. Next, remove the first two lines of the Board
function that call useState
:
function Board({ xIsNext, squares, onPlay }) {
function handleClick(i) {
//...
}
// ...
}
Now replace the setSquares
and setXIsNext
calls in handleClick
in the Board
component with a single call to your new onPlay
function so the Game
component can update the Board
when the user clicks a square:
function Board({ xIsNext, squares, onPlay }) {
function handleClick(i) {
if (calculateWinner(squares) || squares[i]) {
return;
}
const nextSquares = squares.slice();
if (xIsNext) {
nextSquares[i] = "X";
} else {
nextSquares[i] = "O";
}
onPlay(nextSquares);
}
//...
}
The Board
component is fully controlled by the props passed to it by the Game
component. You need to implement the handlePlay
function in the Game
component to get the game working again.
What should handlePlay
do when called? Remember that Board used to call setSquares
with an updated array; now it passes the updated squares
array to onPlay
.
The handlePlay
function needs to update Game
’s state to trigger a re-render, but you don’t have a setSquares
function that you can call any more—you’re now using the history
state variable to store this information. You’ll want to update history
by appending the updated squares
array as a new history entry. You also want to toggle xIsNext
, just as Board used to do:
export default function Game() {
//...
function handlePlay(nextSquares) {
setHistory([...history, nextSquares]);
setXIsNext(!xIsNext);
}
//...
}
Here, [...history, nextSquares]
creates a new array that contains all the items in history
, followed by nextSquares
. (You can read the ...history
spread syntax as “enumerate all the items in history
”.)
For example, if history
is [[null,null,null], ["X",null,null]]
and nextSquares
is ["X",null,"O"]
, then the new [...history, nextSquares]
array will be [[null,null,null], ["X",null,null], ["X",null,"O"]]
.
At this point, you’ve moved the state to live in the Game
component, and the UI should be fully working, just as it was before the refactor. Here is what the code should look like at this point:
import { useState } from 'react'; function Square({ value, onSquareClick }) { return ( <button className="square" onClick={onSquareClick}> {value} </button> ); } function Board({ xIsNext, squares, onPlay }) { function handleClick(i) { if (calculateWinner(squares) || squares[i]) { return; } const nextSquares = squares.slice(); if (xIsNext) { nextSquares[i] = 'X'; } else { nextSquares[i] = 'O'; } onPlay(nextSquares); } const winner = calculateWinner(squares); let status; if (winner) { status = 'Winner: ' + winner; } else { status = 'Next player: ' + (xIsNext ? 'X' : 'O'); } return ( <> <div className="status">{status}</div> <div className="board-row"> <Square value={squares[0]} onSquareClick={() => handleClick(0)} /> <Square value={squares[1]} onSquareClick={() => handleClick(1)} /> <Square value={squares[2]} onSquareClick={() => handleClick(2)} /> </div> <div className="board-row"> <Square value={squares[3]} onSquareClick={() => handleClick(3)} /> <Square value={squares[4]} onSquareClick={() => handleClick(4)} /> <Square value={squares[5]} onSquareClick={() => handleClick(5)} /> </div> <div className="board-row"> <Square value={squares[6]} onSquareClick={() => handleClick(6)} /> <Square value={squares[7]} onSquareClick={() => handleClick(7)} /> <Square value={squares[8]} onSquareClick={() => handleClick(8)} /> </div> </> ); } export default function Game() { const [xIsNext, setXIsNext] = useState(true); const [history, setHistory] = useState([Array(9).fill(null)]); const currentSquares = history[history.length - 1]; function handlePlay(nextSquares) { setHistory([...history, nextSquares]); setXIsNext(!xIsNext); } return ( <div className="game"> <div className="game-board"> <Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} /> </div> <div className="game-info"> <ol>{/*TODO*/}</ol> </div> </div> ); } function calculateWinner(squares) { const lines = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6], ]; for (let i = 0; i < lines.length; i++) { const [a, b, c] = lines[i]; if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) { return squares[a]; } } return null; }
Aikaisempien liikkeiden näyttäminen
Since you are recording the tic-tac-toe game’s history, you can now display a list of past moves to the player.
React elements like <button>
are regular JavaScript objects; you can pass them around in your application. To render multiple items in React, you can use an array of React elements.
You already have an array of history
moves in state, so now you need to transform it to an array of React elements. In JavaScript, to transform one array into another, you can use the array map
method:
[1, 2, 3].map((x) => x * 2) // [2, 4, 6]
You’ll use map
to transform your history
of moves into React elements representing buttons on the screen, and display a list of buttons to “jump” to past moves. Let’s map
over the history
in the Game component:
export default function Game() {
const [xIsNext, setXIsNext] = useState(true);
const [history, setHistory] = useState([Array(9).fill(null)]);
const currentSquares = history[history.length - 1];
function handlePlay(nextSquares) {
setHistory([...history, nextSquares]);
setXIsNext(!xIsNext);
}
function jumpTo(nextMove) {
// TODO
}
const moves = history.map((squares, move) => {
let description;
if (move > 0) {
description = 'Go to move #' + move;
} else {
description = 'Go to game start';
}
return (
<li>
<button onClick={() => jumpTo(move)}>{description}</button>
</li>
);
});
return (
<div className="game">
<div className="game-board">
<Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
</div>
<div className="game-info">
<ol>{moves}</ol>
</div>
</div>
);
}
You can see what your code should look like below. Note that you should see an error in the developer tools console that says: Warning: Each child in an array or iterator should have a unique "key" prop. Check the render method of `Game`.
You’ll fix this error in the next section.
import { useState } from 'react'; function Square({ value, onSquareClick }) { return ( <button className="square" onClick={onSquareClick}> {value} </button> ); } function Board({ xIsNext, squares, onPlay }) { function handleClick(i) { if (calculateWinner(squares) || squares[i]) { return; } const nextSquares = squares.slice(); if (xIsNext) { nextSquares[i] = 'X'; } else { nextSquares[i] = 'O'; } onPlay(nextSquares); } const winner = calculateWinner(squares); let status; if (winner) { status = 'Winner: ' + winner; } else { status = 'Next player: ' + (xIsNext ? 'X' : 'O'); } return ( <> <div className="status">{status}</div> <div className="board-row"> <Square value={squares[0]} onSquareClick={() => handleClick(0)} /> <Square value={squares[1]} onSquareClick={() => handleClick(1)} /> <Square value={squares[2]} onSquareClick={() => handleClick(2)} /> </div> <div className="board-row"> <Square value={squares[3]} onSquareClick={() => handleClick(3)} /> <Square value={squares[4]} onSquareClick={() => handleClick(4)} /> <Square value={squares[5]} onSquareClick={() => handleClick(5)} /> </div> <div className="board-row"> <Square value={squares[6]} onSquareClick={() => handleClick(6)} /> <Square value={squares[7]} onSquareClick={() => handleClick(7)} /> <Square value={squares[8]} onSquareClick={() => handleClick(8)} /> </div> </> ); } export default function Game() { const [xIsNext, setXIsNext] = useState(true); const [history, setHistory] = useState([Array(9).fill(null)]); const currentSquares = history[history.length - 1]; function handlePlay(nextSquares) { setHistory([...history, nextSquares]); setXIsNext(!xIsNext); } function jumpTo(nextMove) { // TODO } const moves = history.map((squares, move) => { let description; if (move > 0) { description = 'Go to move #' + move; } else { description = 'Go to game start'; } return ( <li> <button onClick={() => jumpTo(move)}>{description}</button> </li> ); }); return ( <div className="game"> <div className="game-board"> <Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} /> </div> <div className="game-info"> <ol>{moves}</ol> </div> </div> ); } function calculateWinner(squares) { const lines = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6], ]; for (let i = 0; i < lines.length; i++) { const [a, b, c] = lines[i]; if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) { return squares[a]; } } return null; }
As you iterate through history
array inside the function you passed to map
, the squares
argument goes through each element of history
, and the move
argument goes through each array index: 0
, 1
, 2
, …. (In most cases, you’d need the actual array elements, but to render a list of moves you will only need indexes.)
For each move in the tic-tac-toe game’s history, you create a list item <li>
which contains a button <button>
. The button has an onClick
handler which calls a function called jumpTo
(that you haven’t implemented yet).
For now, you should see a list of the moves that occurred in the game and an error in the developer tools console. Let’s discuss what the “key” error means.
Avaimen valinta
When you render a list, React stores some information about each rendered list item. When you update a list, React needs to determine what has changed. You could have added, removed, re-arranged, or updated the list’s items.
Imagine transitioning from
<li>Alexa: 7 tasks left</li>
<li>Ben: 5 tasks left</li>
to
<li>Ben: 9 tasks left</li>
<li>Claudia: 8 tasks left</li>
<li>Alexa: 5 tasks left</li>
In addition to the updated counts, a human reading this would probably say that you swapped Alexa and Ben’s ordering and inserted Claudia between Alexa and Ben. However, React is a computer program and can’t know what you intended, so you need to specify a key property for each list item to differentiate each list item from its siblings. If your data was from a database, Alexa, Ben, and Claudia’s database IDs could be used as keys.
<li key={user.id}>
{user.name}: {user.taskCount} tasks left
</li>
When a list is re-rendered, React takes each list item’s key and searches the previous list’s items for a matching key. If the current list has a key that didn’t exist before, React creates a component. If the current list is missing a key that existed in the previous list, React destroys the previous component. If two keys match, the corresponding component is moved.
Keys tell React about the identity of each component, which allows React to maintain state between re-renders. If a component’s key changes, the component will be destroyed and re-created with a new state.
key
is a special and reserved property in React. When an element is created, React extracts the key
property and stores the key directly on the returned element. Even though key
may look like it is passed as props, React automatically uses key
to decide which components to update. There’s no way for a component to ask what key
its parent specified.
It’s strongly recommended that you assign proper keys whenever you build dynamic lists. If you don’t have an appropriate key, you may want to consider restructuring your data so that you do.
If no key is specified, React will report an error and use the array index as a key by default. Using the array index as a key is problematic when trying to re-order a list’s items or inserting/removing list items. Explicitly passing key={i}
silences the error but has the same problems as array indices and is not recommended in most cases.
Keys do not need to be globally unique; they only need to be unique between components and their siblings.
Aikamatkustuksen toteutus
In the tic-tac-toe game’s history, each past move has a unique ID associated with it: it’s the sequential number of the move. Moves will never be re-ordered, deleted, or inserted in the middle, so it’s safe to use the move index as a key.
In the Game
function, you can add the key as <li key={move}>
, and if you reload the rendered game, React’s “key” error should disappear:
const moves = history.map((squares, move) => {
//...
return (
<li key={move}>
<button onClick={() => jumpTo(move)}>{description}</button>
</li>
);
});
import { useState } from 'react'; function Square({ value, onSquareClick }) { return ( <button className="square" onClick={onSquareClick}> {value} </button> ); } function Board({ xIsNext, squares, onPlay }) { function handleClick(i) { if (calculateWinner(squares) || squares[i]) { return; } const nextSquares = squares.slice(); if (xIsNext) { nextSquares[i] = 'X'; } else { nextSquares[i] = 'O'; } onPlay(nextSquares); } const winner = calculateWinner(squares); let status; if (winner) { status = 'Winner: ' + winner; } else { status = 'Next player: ' + (xIsNext ? 'X' : 'O'); } return ( <> <div className="status">{status}</div> <div className="board-row"> <Square value={squares[0]} onSquareClick={() => handleClick(0)} /> <Square value={squares[1]} onSquareClick={() => handleClick(1)} /> <Square value={squares[2]} onSquareClick={() => handleClick(2)} /> </div> <div className="board-row"> <Square value={squares[3]} onSquareClick={() => handleClick(3)} /> <Square value={squares[4]} onSquareClick={() => handleClick(4)} /> <Square value={squares[5]} onSquareClick={() => handleClick(5)} /> </div> <div className="board-row"> <Square value={squares[6]} onSquareClick={() => handleClick(6)} /> <Square value={squares[7]} onSquareClick={() => handleClick(7)} /> <Square value={squares[8]} onSquareClick={() => handleClick(8)} /> </div> </> ); } export default function Game() { const [xIsNext, setXIsNext] = useState(true); const [history, setHistory] = useState([Array(9).fill(null)]); const currentSquares = history[history.length - 1]; function handlePlay(nextSquares) { setHistory([...history, nextSquares]); setXIsNext(!xIsNext); } function jumpTo(nextMove) { // TODO } const moves = history.map((squares, move) => { let description; if (move > 0) { description = 'Go to move #' + move; } else { description = 'Go to game start'; } return ( <li key={move}> <button onClick={() => jumpTo(move)}>{description}</button> </li> ); }); return ( <div className="game"> <div className="game-board"> <Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} /> </div> <div className="game-info"> <ol>{moves}</ol> </div> </div> ); } function calculateWinner(squares) { const lines = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6], ]; for (let i = 0; i < lines.length; i++) { const [a, b, c] = lines[i]; if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) { return squares[a]; } } return null; }
Before you can implement jumpTo
, you need the Game
component to keep track of which step the user is currently viewing. To do this, define a new state variable called currentMove
, defaulting to 0
:
export default function Game() {
const [xIsNext, setXIsNext] = useState(true);
const [history, setHistory] = useState([Array(9).fill(null)]);
const [currentMove, setCurrentMove] = useState(0);
const currentSquares = history[history.length - 1];
//...
}
Next, update the jumpTo
function inside Game
to update that currentMove
. You’ll also set xIsNext
to true
if the number that you’re changing currentMove
to is even.
export default function Game() {
// ...
function jumpTo(nextMove) {
setCurrentMove(nextMove);
setXIsNext(nextMove % 2 === 0);
}
//...
}
You will now make two changes to the Game
’s handlePlay
function which is called when you click on a square.
- If you “go back in time” and then make a new move from that point, you only want to keep the history up to that point. Instead of adding
nextSquares
after all items (...
spread syntax) inhistory
, you’ll add it after all items inhistory.slice(0, currentMove + 1)
so that you’re only keeping that portion of the old history. - Each time a move is made, you need to update
currentMove
to point to the latest history entry.
function handlePlay(nextSquares) {
const nextHistory = [...history.slice(0, currentMove + 1), nextSquares];
setHistory(nextHistory);
setCurrentMove(nextHistory.length - 1);
setXIsNext(!xIsNext);
}
Finally, you will modify the Game
component to render the currently selected move, instead of always rendering the final move:
export default function Game() {
const [xIsNext, setXIsNext] = useState(true);
const [history, setHistory] = useState([Array(9).fill(null)]);
const [currentMove, setCurrentMove] = useState(0);
const currentSquares = history[currentMove];
// ...
}
If you click on any step in the game’s history, the tic-tac-toe board should immediately update to show what the board looked like after that step occurred.
import { useState } from 'react'; function Square({value, onSquareClick}) { return ( <button className="square" onClick={onSquareClick}> {value} </button> ); } function Board({ xIsNext, squares, onPlay }) { function handleClick(i) { if (calculateWinner(squares) || squares[i]) { return; } const nextSquares = squares.slice(); if (xIsNext) { nextSquares[i] = 'X'; } else { nextSquares[i] = 'O'; } onPlay(nextSquares); } const winner = calculateWinner(squares); let status; if (winner) { status = 'Winner: ' + winner; } else { status = 'Next player: ' + (xIsNext ? 'X' : 'O'); } return ( <> <div className="status">{status}</div> <div className="board-row"> <Square value={squares[0]} onSquareClick={() => handleClick(0)} /> <Square value={squares[1]} onSquareClick={() => handleClick(1)} /> <Square value={squares[2]} onSquareClick={() => handleClick(2)} /> </div> <div className="board-row"> <Square value={squares[3]} onSquareClick={() => handleClick(3)} /> <Square value={squares[4]} onSquareClick={() => handleClick(4)} /> <Square value={squares[5]} onSquareClick={() => handleClick(5)} /> </div> <div className="board-row"> <Square value={squares[6]} onSquareClick={() => handleClick(6)} /> <Square value={squares[7]} onSquareClick={() => handleClick(7)} /> <Square value={squares[8]} onSquareClick={() => handleClick(8)} /> </div> </> ); } export default function Game() { const [xIsNext, setXIsNext] = useState(true); const [history, setHistory] = useState([Array(9).fill(null)]); const [currentMove, setCurrentMove] = useState(0); const currentSquares = history[currentMove]; function handlePlay(nextSquares) { const nextHistory = [...history.slice(0, currentMove + 1), nextSquares]; setHistory(nextHistory); setCurrentMove(nextHistory.length - 1); setXIsNext(!xIsNext); } function jumpTo(nextMove) { setCurrentMove(nextMove); setXIsNext(nextMove % 2 === 0); } const moves = history.map((squares, move) => { let description; if (move > 0) { description = 'Go to move #' + move; } else { description = 'Go to game start'; } return ( <li key={move}> <button onClick={() => jumpTo(move)}>{description}</button> </li> ); }); return ( <div className="game"> <div className="game-board"> <Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} /> </div> <div className="game-info"> <ol>{moves}</ol> </div> </div> ); } function calculateWinner(squares) { const lines = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6], ]; for (let i = 0; i < lines.length; i++) { const [a, b, c] = lines[i]; if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) { return squares[a]; } } return null; }
Loppusiivous
If you look at the code very closely, you may notice that xIsNext === true
when currentMove
is even and xIsNext === false
when currentMove
is odd. In other words, if you know the value of currentMove
, then you can always figure out what xIsNext
should be.
There’s no reason for you to store both of these in state. In fact, always try to avoid redundant state. Simplifying what you store in state reduces bugs and makes your code easier to understand. Change Game
so that it doesn’t store xIsNext
as a separate state variable and instead figures it out based on the currentMove
:
export default function Game() {
const [history, setHistory] = useState([Array(9).fill(null)]);
const [currentMove, setCurrentMove] = useState(0);
const xIsNext = currentMove % 2 === 0;
const currentSquares = history[currentMove];
function handlePlay(nextSquares) {
const nextHistory = [...history.slice(0, currentMove + 1), nextSquares];
setHistory(nextHistory);
setCurrentMove(nextHistory.length - 1);
}
function jumpTo(nextMove) {
setCurrentMove(nextMove);
}
// ...
}
You no longer need the xIsNext
state declaration or the calls to setXIsNext
. Now, there’s no chance for xIsNext
to get out of sync with currentMove
, even if you make a mistake while coding the components.
Lopetus
Congratulations! You’ve created a tic-tac-toe game that:
- Lets you play tic-tac-toe,
- Indicates when a player has won the game,
- Stores a game’s history as a game progresses,
- Allows players to review a game’s history and see previous versions of a game’s board.
Nice work! We hope you now feel like you have a decent grasp of how React works.
Check out the final result here:
import { useState } from 'react'; function Square({ value, onSquareClick }) { return ( <button className="square" onClick={onSquareClick}> {value} </button> ); } function Board({ xIsNext, squares, onPlay }) { function handleClick(i) { if (calculateWinner(squares) || squares[i]) { return; } const nextSquares = squares.slice(); if (xIsNext) { nextSquares[i] = 'X'; } else { nextSquares[i] = 'O'; } onPlay(nextSquares); } const winner = calculateWinner(squares); let status; if (winner) { status = 'Winner: ' + winner; } else { status = 'Next player: ' + (xIsNext ? 'X' : 'O'); } return ( <> <div className="status">{status}</div> <div className="board-row"> <Square value={squares[0]} onSquareClick={() => handleClick(0)} /> <Square value={squares[1]} onSquareClick={() => handleClick(1)} /> <Square value={squares[2]} onSquareClick={() => handleClick(2)} /> </div> <div className="board-row"> <Square value={squares[3]} onSquareClick={() => handleClick(3)} /> <Square value={squares[4]} onSquareClick={() => handleClick(4)} /> <Square value={squares[5]} onSquareClick={() => handleClick(5)} /> </div> <div className="board-row"> <Square value={squares[6]} onSquareClick={() => handleClick(6)} /> <Square value={squares[7]} onSquareClick={() => handleClick(7)} /> <Square value={squares[8]} onSquareClick={() => handleClick(8)} /> </div> </> ); } export default function Game() { const [history, setHistory] = useState([Array(9).fill(null)]); const [currentMove, setCurrentMove] = useState(0); const xIsNext = currentMove % 2 === 0; const currentSquares = history[currentMove]; function handlePlay(nextSquares) { const nextHistory = [...history.slice(0, currentMove + 1), nextSquares]; setHistory(nextHistory); setCurrentMove(nextHistory.length - 1); } function jumpTo(nextMove) { setCurrentMove(nextMove); } const moves = history.map((squares, move) => { let description; if (move > 0) { description = 'Go to move #' + move; } else { description = 'Go to game start'; } return ( <li key={move}> <button onClick={() => jumpTo(move)}>{description}</button> </li> ); }); return ( <div className="game"> <div className="game-board"> <Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} /> </div> <div className="game-info"> <ol>{moves}</ol> </div> </div> ); } function calculateWinner(squares) { const lines = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6], ]; for (let i = 0; i < lines.length; i++) { const [a, b, c] = lines[i]; if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) { return squares[a]; } } return null; }
If you have extra time or want to practice your new React skills, here are some ideas for improvements that you could make to the tic-tac-toe game, listed in order of increasing difficulty:
- For the current move only, show “You are at move #…” instead of a button.
- Rewrite
Board
to use two loops to make the squares instead of hardcoding them. - Add a toggle button that lets you sort the moves in either ascending or descending order.
- When someone wins, highlight the three squares that caused the win (and when no one wins, display a message about the result being a draw).
- Display the location for each move in the format (row, col) in the move history list.
Throughout this tutorial, you’ve touched on React concepts including elements, components, props, and state. Now that you’ve seen how these concepts work when building a game, check out Thinking in React to see how the same React concepts work when build an app’s UI.