Tutorijal: Iks-Oks
Tokom ovog tutorijala napravićete jednostavnu igru iks-oks. Ovaj tutorijal ne zahteva prethodno znanje o React-u. Tehnike koje ćete naučiti su fundamentalne za pravljenje bilo koje React aplikacije, a potpuno razumevanje ovog tutorijala će vam pružiti duboko razumevanje React-a.
Tutorijal je podeljen u nekoliko sekcija:
- Setup tutorijala pružiće vam početnu tačku za praćenje tutorijala.
- Pregled će vas naučiti osnovama React-a: component-ama, props-ima i state-u.
- Završavanje igre će vas naučiti najčešćim tehnikama u radu sa React-om.
- Dodavanje putovanja kroz vreme pružiće vam dublji uvid u jedinstvene prednosti React-a.
Šta pravite?
U ovom tutorijalu napravićete interaktivnu igru iks-oks koristeći React.
Ovde možete videti kako će izgledati gotov projekat:
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 = 'Pobednik: ' + winner; } else { status = 'Sledeći igrač : ' + (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 = 'Prebacite se na potez #' + move; } else { description = 'Prebacite se na početak'; } 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; }
Ako vam kod još uvek nije jasan, ili ako niste upoznati sa sintaksom, ne brinite! Cilj ovog tutorijala je da vam pomogne da razumete React i njegovu sintaksu.
Preporučujemo vam da se prvo poigrate sa završenom igrom pre nego što nastavite sa tutorijalom. Jedna od funkcionalnosti koju ćete primetiti je numerisana lista sa desne strane table igre. Ova lista vam pruža istoriju svih poteza koji su se odigrali tokom igre, i update-je se kako igra napreduje.
Nakon što se upoznate sa završenom igrom, nastavite sa čitanjem. Sledeći korak je da vas postavimo u poziciju da počnete sa pravljenjem igre.
Setup tutorijala
U live code editor ispod, kliknite na Fork u gornjem desnom uglu kako bi otvorili editor u novom tabu koristeći CodeSandbox. CodeSandbox vam omogućava da pišete kod direktno u browseru i pregledate kako će aplikacija izgledati korisnicima. Novi tab bi trebalo da prikaže prazan kvadrat i početni kod za ovaj tutorijal.
export default function Square() { return <button className="square">X</button>; }
Pregled
Sada kada ste postavili okruženje, hajde da napravimo pregled React-a!
Pregled starter koda
U CodeSandbox-u ćete videti tri glavne sekcije:
- Sekcija Files sa listom fajlova kao što su
App.js
,index.js
,styles.css
i folder pod nazivompublic
- code editor gde ćete videti source kod odabranog fajla
- Sekcija browser gde ćete videti kako će kod koji ste napisali biti prikazan
Fajl App.js
bi trebalo da bude izabran u sekciji Files. Sadržaj tog fajla u code editor bi trebalo da izgleda ovako:
export default function Square() {
return <button className="square">X</button>;
}
Sekcija browser bi trebalo da prikazuje kvadrat sa X u njemu ovako:
Sada, hajde da pogledamo fajlove u starter kodu.
App.js
Kod u fajlu App.js
kreira component-u. U React-u, component-a je deo višekratnog koda koji predstavlja deo korisničkog interfejsa. Component-e se koriste za renderovanje, upravljanje i update-ovanje elemenata korisničkog interfejsa u vašoj aplikaciji. Hajde da analiziramo component-u liniju po liniju kako bismo uočili šta se dešava:
export default function Square() {
return <button className="square">X</button>;
}
Prva linija definiše funkciju pod nazivom Square
. JavaScript ključna reč export
omogućava da ova funkcija bude dostupna van ovog fajla. Ključna reč default
označava da je to glavna funkcija u vašem fajlu koju će drugi fajlovi koristiti.
export default function Square() {
return <button className="square">X</button>;
}
Druga linija return-uje dugme. JavaScript ključna reč return
znači da se ono što dolazi posle nje vraća kao vrednost pozivaocu funkcije. <button>
je JSX element. JSX element je kombinacija JavaScript koda i HTML oznaka koja opisuje šta želite da prikažete. className="square"
je svojstvo dugmeta ili prop koje CSS-u govori kako da stilizuje dugme. X
je tekst koji se prikazuje unutar dugmeta, a </button>
zatvara JSX element, označavajući da bilo koji sadržaj nakon toga ne treba da bude postavljen unutar dugmeta.
styles.css
Kliknite na fajl pod nazivom styles.css
u sekciji Files u CodeSandbox-u. Ovaj fajl definiše stilove za vašu React aplikaciju. Prva dva CSS selektora (*
i body
) definišu stil za veće delove vaše aplikacije, dok selektor .square
definiše stil za bilo koju component-u gde je svojstvo className
postavljeno na square
. U vašem kodu, to bi odgovaralo dugmetu iz component-e Square u fajlu App.js
.
index.js
Kliknite na fajl pod nazivom index.js
u sekciji Files u CodeSandbox-u. Nećete editovati ovaj fajl tokom tutorijala, ali on predstavlja sponu između component-e koju ste kreirali u fajlu App.js
i web browsera.
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import './styles.css';
import App from './App';
Linije 1-5 spajaju sve potrebne delove zajedno:
- React
- React-ov library za komunikaciju sa web browserima (React DOM)
- stilove za vaše component-e
- component-u koju ste kreirali u
App.js
.
Ostatak fajla spaja sve delove i ubacuje finalni proizvod u index.html
u folderu public
.
Kreiranje table
Vratimo se na App.js
. Ovdee ćete provesti ostatak tutorijala.
Trenutno je tabla samo jedan kvadrat, ali vam je neophodno devet! Ukoliko pokušate samo da kopirate i nalepite vaš kvadrat kako biste napravili dva kvadrata ovako:
export default function Square() {
return <button className="square">X</button><button className="square">X</button>;
}
Dobićete ovu grešku:
<>...</>
?React component-e moraju da vraćaju jedinstven JSX element, a ne više susednih JSX elemenata poput dva dugmeta. Da biste to ispravili, možete koristiti Fragmente (<>
i </>
) kako biste obuhvatili više susednih JSX elemenata ovako:
export default function Square() {
return (
<>
<button className="square">X</button>
<button className="square">X</button>
</>
);
}
Sada bi trebalo da vidite:
Odlično! Sada samo treba nekoliko puta da kopirate i nalepite kako biste dodali devet kvadrata i…
Oh ne! Svi kvadrati su u jednoj liniji, a ne u mreži kakva vam je potrebna za našu tablu. Da biste to ispravili, moraćete da grupišete kvadrate u redove koristeći div
i dodate nekoliko CSS klasa. Usput, dodelićete svakom kvadratu broj kako biste bili sigurni gde je svaki kvadrat prikazan.
U fajlu App.js
, update-ujete component-u Square
da izgleda ovako:
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>
</>
);
}
CSS definisan u fajlu styles.css
stilizuje div-ove sa className
postavljenim na board-row
. Sada kada ste grupisali svoje component-e u redove sa stilizovanim div
-ovima, imate vašu iks-oks tablu:
Ali sada imate problem. Vaša component-a pod nazivom Square
više zapravo nije kvadrat. Hajde da to ispravimo promenom imena u Board
:
export default function Board() {
//...
}
U ovom trenutku vaš kod bi trebalo da izgleda ovako:
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> </> ); }
Prosleđivanje podataka putem props-a
Sledeće, želećete da promenite vrednost kvadrata iz praznog u “X” kada korisnik klikne na kvadrat. Na osnovu toga kako ste do sada izgradili tablu, morali biste da kopirate i nalepite kod koji update-uje kvadrat devet puta (jednom za svaki kvadrat koji imate)! Umesto kopiranja i lepljenja, React-ova component-na arhitektura vam omogućava da kreirate ponovo iskoristivu component-u kako biste izbegli nered i dupliran kod.
Prvo, kopiraćete liniju koja definiše vaš prvi kvadrat (<button className="square">1</button>
) iz component-e Board
u novu component-u Square
:
function Square() {
return <button className="square">1</button>;
}
export default function Board() {
// ...
}
Zatim ćete update-ovati Board component-u da renderuje ovu Square
component-u koristeći JSX sintaksu:
// ...
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>
</>
);
}
Obratite pažnju na to da, za razliku od browser-ovih div
-ova, vaše component-e Board
i Square
moraju počinjati velikim slovom.
Hajde da pogledamo:
Oh ne! Izgubili ste numerisane kvadrate koje ste imali ranije. Sada svaki kvadrat prikazuje “1”. Da biste to ispravili, koristićete props za prosleđivanje vrednosti koju svaki kvadrat treba da ima iz parent component-e (Board
) ka njenom child-u (Square
).
Update-ujte component-u Square
da koristi value
prop koji ćete proslediti iz component-e Board
:
function Square({ value }) {
return <button className="square">1</button>;
}
function Square({ value })
označava da Square component-a može primiti prop pod nazivom value
.
Sada želite da prikažete taj value
umesto 1
unutar svakog kvadrata. Pokušajte to da uradite ovako:
function Square({ value }) {
return <button className="square">value</button>;
}
Ups, ovo nije ono što ste želeli:
Želeli ste da renderujete JavaScript promenljivu pod nazivom value
iz vaše component-e, a ne reč “value”. Da biste “pobegli u JavaScript” iz JSX-a, potrebno je da koristite vitičaste (kovrdžave) zagrade. Dodajte vitičaste zagrade oko value
u JSX-u ovako:
function Square({ value }) {
return <button className="square">{value}</button>;
}
Za sada, trebalo bi da vidite praznu tablu:
To je zato što component-a Board
još uvek nije prosledila value
prop svakoj component-i Square
koju renderuje. Da biste to ispravili, dodaćete value
prop svakoj component-i Square
koju renderuje component-a Board
:
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>
</>
);
}
Sada bi trebalo ponovo da vidite mrežu sa brojevima:
Vaš update-ovani kod bi trebalo da izgleda ovako:
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> </> ); }
Izrada interaktivne component-e
Hajde da ispunimo Square
component-u sa X
kada kliknete na nju. Deklarišite funkciju pod nazivom handleClick
unutar component-e Square
. Zatim, dodajte onClick
u props dugmeta koje se vraća iz JSX elementa u Square
component-i:
function Square({ value }) {
function handleClick() {
console.log('clicked!');
}
return (
<button
className="square"
onClick={handleClick}
>
{value}
</button>
);
}
Ukoliko sada kliknete na kvadrat, trebalo bi da vidite log sa porukom "clicked!"
u Console tab-u na dnu sekcije Browser u CodeSandbox-u. Klikovima na kvadrat više puta, logovaće se "clicked!"
ponovo. Ponavljajući logovi sa istom porukom neće kreirati nove linije u konzoli. Umesto toga, videćete brojač koji se povećava pored vašeg prvog loga "clicked!"
.
Sledeći korak je da Square
component-a “zapamti” da je kliknuta i da je ispuni oznakom “X”. Da bi “zapamtile” stvari, component-e koriste state.
React obezbeđuje posebnu funkciju pod nazivom useState
koju možete pozvati iz svoje component-e kako bi ona mogla da “pamti” stvari. Hajde da sačuvamo trenutnu vrednost Square
component-e u state-u i promenimo je kada se klikne na Square
.
Importujte useState
na vrhu fajla. Uklonite value
prop iz component-e Square
. Umesto toga, dodajte novu liniju na početku Square
component-e koja poziva useState
. Neka ona vrati promenljivu state-a pod nazivom value
:
import { useState } from 'react';
function Square() {
const [value, setValue] = useState(null);
function handleClick() {
//...
value
čuva vrednost, a setValue
je funkcija koja se koristi za promenu te vrednosti. null
koji je prosleđen u useState
koristi se kao početna vrednost za ovu promenljivu stanja, tako da value
ovde počinje sa vrednošću jednakom null
.
Pošto component-a Square
više ne prihvata props, uklonićete value
prop iz svih devet Square
component-i koje kreira component-a Board
:
// ...
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>
</>
);
}
Sada ćete promeniti component-u Square
da prikaže “X” kada se klikne. Zamenite event handler console.log("clicked!");
sa setValue('X');
. Sada vaša Square
component-a izgleda ovako:
function Square() {
const [value, setValue] = useState(null);
function handleClick() {
setValue('X');
}
return (
<button
className="square"
onClick={handleClick}
>
{value}
</button>
);
}
Pozivanjem ove set
funkcije iz onClick
handler-a, govorite React-u da ponovo renderuje taj Square
kad god se klikne njegov <button>
. Nakon update-a, value
component-e Square
biće 'X'
, tako da ćete videti “X” na tabli. Kliknite na bilo koji kvadrat i “X” bi trebalo da se pojavi:
Svaki kvadrat ima svoj state: value
sačuvano u svakom kvadratu je potpuno nezavisno od drugih. Kada pozovete set
funkciju u component-i, React automatski update-uje i njene unutarnje child component-e.
Nakon što napravite gore navedene izmene, vaš kod će izgledati ovako:
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 Developer Tools
React DevTools vam omogućava da proverite props i stanje vaših React component-i. Možete pronaći karticu React DevTools na dnu sekcije browser u CodeSandbox-u:
Da biste pregledali određenu component-u na ekranu, koristite dugme u gornjem levom uglu React DevTools-a:
Završavanje igre
Do ovog trenutka, imate sve osnovne građevinske blokove za vašu iks-oks igru. Da biste je kompletirali, potrebno da naizmenično postavljate “X” i “O” na tablu, i potreban vam je način da odredite pobednika.
Podizanje state-a
Trenutno svaka Square
component-a čuva deo state-a igre. Da bi se proverilo ko je pobednik u igri iks-oks, Board
bi morao nekako da zna state svake od 9 Square
component-i.
Kako biste to rešili? Možda biste pomislili da Board
treba da „pita” svaku Square
component-u za njen state. Iako je ovaj pristup tehnički moguć u React-u, ne preporučujemo ga jer kod postaje težak za razumevanje, podložan je greškama i teško ga je refaktorisati. Umesto toga, najbolji pristup je da se state igre čuva u parent component-i Board
, umesto u svakoj Square
component-i. component-a Board
može da kaže svakoj Square
component-i šta treba da prikaže prosleđivanjem props-a, kao što ste ranije prosleđivali broj svakoj Square
component-i.
Da biste sakupili podatke od više child component-i ili omogućili komunikaciju između dve child component-e, definišite zajednički state u njihovoj parent component-i. Parent component-a može da prosledi taj state nazad child component-ama putem props-a. Na ovaj način child component-e ostaju sinhroarray-ovane međusobno i sa parent component-om.
Podizanje state-a u parent component-u je uobičajena praksa pri refaktorisanju React component-i.
Hajde da iskoristimo ovu priliku i isprobamo ovo. Izmenite component-u Board
tako da deklariše varijablu state-a pod nazivom squares
, koja ima podrazumevanu vrednost u vidu array-a od 9 null
vrednosti koje odgovaraju za svaki od 9 kvadrata:
// ...
export default function Board() {
const [squares, setSquares] = useState(Array(9).fill(null));
return (
// ...
);
}
Array(9).fill(null)
kreira array sa devet elemenata i postavlja svaki od njih na null
. Poziv useState()
oko njega deklariše varijablu state-a pod nazivom squares
, koja je inicijalno postavljena na taj array. Svaki element u array-u odgovara vrednosti jednog kvadrata. Kada kasnije popunite tablu, array squares
će izgledati ovako:
['O', null, 'X', 'X', 'X', 'O', 'O', null, null]
Sada vaša Board
component-a treba da prosledi value
prop svakoj Square
component-i koju renderuje:
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>
</>
);
}
Zatim ćete izmeniti Square
component-u da prima value
prop iz Board
component-e. Ovo će zahtevati uklanjanje sopstvenog state-a value
iz Square
component-e, kao i onClick
props-a dugmeta:
function Square({value}) {
return <button className="square">{value}</button>;
}
U ovom trenutku trebalo bi da vidite praznu tablu za igru iks-oks:
A vaš kod bi trebalo da izgleda ovako:
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> </> ); }
Svaka Square
component-a će sada primati value
prop koji može imati vrednost 'X'
, 'O'
ili null
za prazne kvadrate.
Sledeće, treba da promenite šta se dešava kada se klikne na Square
. Board
component-a sada vodi računa o tome koji su kvadrati popunjeni. Biće vam potrebno da napravite način na koji Square
može da update-uje state component-e Board
. Pošto je state privatan za component-u koja ga definiše, ne možete update-ovati state component-e Board
direktno iz Square
.
Umesto toga, prosledićete funkciju iz Board
component-e u Square
component-u, i Square
će pozvati tu funkciju kada se na nju klikne. Počećete sa funkcijom koju će Square
component-a pozvati kada se klikne na nju. Tu funkciju ćete nazvati onSquareClick
:
function Square({ value }) {
return (
<button className="square" onClick={onSquareClick}>
{value}
</button>
);
}
Zatim, dodaćete funkciju onSquareClick
u props Square
component-e:
function Square({ value, onSquareClick }) {
return (
<button className="square" onClick={onSquareClick}>
{value}
</button>
);
}
Sada ćete povezati onSquareClick
prop sa funkcijom u Board
component-i koju ćete nazvati handleClick
. Da biste povezali onSquareClick
sa handleClick
, prosledićete funkciju kao vrednost onSquareClick
prop-u prve Square
component-e:
export default function Board() {
const [squares, setSquares] = useState(Array(9).fill(null));
return (
<>
<div className="board-row">
<Square value={squares[0]} onSquareClick={handleClick} />
//...
);
}
Na kraju, definisaćete funkciju handleClick
unutar Board
component-e kako biste update-ovali array- squares
koji čuva state vaše table:
export default function Board() {
const [squares, setSquares] = useState(Array(9).fill(null));
function handleClick() {
const nextSquares = squares.slice();
nextSquares[0] = "X";
setSquares(nextSquares);
}
return (
// ...
)
}
Funkcija handleClick
kreira kopiju array-a squares
(nextSquares
) korišćenjem JavaScript metode slice()
za array-ove. Zatim, handleClick
update-uje array- nextSquares
tako što dodaje X
na prvi kvadrat (indeks [0]
).
Pozivanjem funkcije setSquares
obaveštavate React da se state component-e promenio. Ovo će pokrenuti ponovno renderovanje component-i koje koriste state squares
(Board
), kao i njenih child component-i (Square
component-e koje čine tablu).
Sada možete dodati X-ove na tablu… ali samo u gornji levi kvadrat. Vaša funkcija handleClick
je trenutno hardkodirana da update-uje indeks gornjeg levog kvadrata (0
). Napravimo update handleClick
tako da može update-ovati bilo koji kvadrat. Dodajte argument i
funkciji handleClick
koji prima indeks kvadrata koji treba update-ovati:
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 (
// ...
)
}
Potom ćete morati da prosledite taj i
funkciji handleClick
. Mogli biste pokušati da postavite onSquareClick
prop kvadrata direktno na handleClick(0)
u JSX-u, ovako, ali to neće funkcionisati:
<Square value={squares[0]} onSquareClick={handleClick(0)} />
Evo zašto ovo ne funkcioniše. Poziv handleClick(0)
postaje deo renderovanja component-e Board
. Pošto handleClick(0)
menja state component-e Board
pozivanjem setSquares
, cela component-a Board
će ponovo biti renderovana. Međutim, to ponovo pokreće handleClick(0)
, što dovodi do infinite loop-a:
Zašto se ovaj problem nije pojavio ranije?
Kada ste prosleđivali onSquareClick={handleClick}
, prosleđivali ste funkciju handleClick
kao prop. Niste je pozivali! Ali sada pozivate tu funkciju odmah—obratite pažnju na zagrade u handleClick(0)
—zbog čega se funkcija pokreće prerano. Ne želite da pozovete handleClick
dok korisnik ne klikne!
Ovaj problem biste mogli rešiti kreiranjem funkcije poput handleFirstSquareClick
, koja poziva handleClick(0)
, funkcije poput handleSecondSquareClick
, koja poziva handleClick(1)
, i tako dalje. Zatim biste te funkcije prosledili (umesto da ih pozovete) kao props, na primer onSquareClick={handleFirstSquareClick}
. Ovo bi rešilo problem infinite loop-a.
Međutim, definisanje devet različitih funkcija i davanje imena svakoj od njih bilo bi previše opširno. Umesto toga, hajde da uradimo sledeće:
export default function Board() {
// ...
return (
<>
<div className="board-row">
<Square value={squares[0]} onSquareClick={() => handleClick(0)} />
// ...
);
}
Obratite pažnju na novu sintaksu () =>
. Ovde, () => handleClick(0)
predstavlja arrow funkciju, koja je kraći način za definisanje funkcija. Kada korisnik klikne na kvadrat, kod koji se nalazi posle strelice =>
će se izvršiti, pozivajući handleClick(0)
.
Sada treba da update-ujete ostalih osam kvadrata kako bi pozivali handleClick
iz arrow funkcija koje prosleđujete. Uverite se da argument za svaki poziv funkcije handleClick
odgovara indeksu odgovarajućeg kvadrata:
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>
</>
);
};
Sada ponovo možete dodavati X-ove na bilo koji kvadrat na tabli klikom na njih:
Ali ovoga puta celokupno upravljanje state-om se obavlja u Board
component-i!
Ovako bi vaš kod trebalo da izgleda:
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> </> ); }
Sada kada se upravljanje state-om nalazi u Board
component-i, parent component-a Board
prosleđuje props child component-ama Square
, omogućavajući im da se ispravno prikažu. Kada kliknete na Square
, child component-a Square
sada traži od parent component-e Board
da update-uje state table. Kada se state Board
component-e promeni, i Board
component-a i svaka child component-a Square
automatski se ponovo renderuju. Čuvanje state-a svih kvadrata u Board
component-i omogućiće joj da u budućnosti odredi pobednika.
Sumirajmo šta se dešava kada korisnik klikne na gornji levi kvadrat na vašoj tabli kako bi dodao X
:
- Klik na gornji levi kvadrat pokreće funkciju koju je
button
dobio kao svojonClick
prop izSquare
component-e.Square
component-a je tu funkciju dobila kao svojonSquareClick
prop izBoard
component-e.Board
component-a je tu funkciju definisala direktno u JSX-u i pozivahandleClick
sa argumentom0
. - Funkcija
handleClick
koristi argument (0
) da update-uje prvi element array-asquares
sanull
naX
. - State
squares
izBoard
component-e se update-uje, pa seBoard
i sve njene child component-e ponovo renderuju. Ovo izaziva promenuvalue
prop-a component-eSquare
sa indeksom0
iznull
uX
.
Na kraju, korisnik vidi da se gornji levi kvadrat promenio iz praznog u kvadrat sa X
nakon što je kliknuo na njega.
Zašto je nepromenljivost važna
Obratite pažnju kako u funkciji handleClick
koristite .slice()
da biste kreirali kopiju array-a squares
umesto da menjate postojeći array. Da bismo objasnili zašto je to važno, moramo razgovarati o nepromenljivosti i zašto je nepromenljivost važna za učenje.
Generalno, postoje dva pristupa promeni podataka. Prvi pristup je menjanje (mutiranje) podataka direktnom promenom njihovih vrednosti. Drugi pristup je zamena podataka novom kopijom koja ima željene izmene. Ovako bi izgledalo kada biste promenili array squares
mutacijom:
const squares = [null, null, null, null, null, null, null, null, null];
squares[0] = 'X';
// Now `squares` is ["X", null, null, null, null, null, null, null, null];
A ovako bi izgledalo kada biste promenili podatke bez mutiranja array-a squares
:
const squares = [null, null, null, null, null, null, null, null, null];
const nextSquares = ['X', null, null, null, null, null, null, null, null];
// Now `squares` is unchanged, but `nextSquares` first element is 'X' rather than `null`
Rezultat je isti, ali time što ne mutirate (ne menjate osnovne podatke) direktno, dobijate nekoliko prednosti.
Nepromenljivost (immutability) čini implementaciju složenih funkcionalnosti mnogo jednostavnijom. Kasnije u ovom tutorijalu implementiraćete funkcionalnost “putovanja kroz vreme” koja vam omogućava pregled istorije igre i “skok” nazad na prethodne poteze. Ova funkcionalnost nije specifična samo za igre—-mogućnost poništavanja i ponavljanja određenih akcija je čest zahtev za aplikacije. Izbegavanje direktne mutacije podataka omogućava vam da prethodne verzije podataka ostanu netaknute i da ih ponovo koristite kasnije.
Postoji još jedna prednost nepromenljivosti. Podrazumevano, sve child component-e se ponovo renderuju automatski kada se state parent component-e promeni. Ovo uključuje čak i child component-e koje nisu bile pogođene promenom. Iako ponovni renderi sami po sebi nisu primetni korisniku (i ne bi trebalo aktivno da ih izbegavate!), možda ćete želeti da preskočite ponovni render dela stabla koji očigledno nije pogođen, iz razloga performansi. Nepromenljivost čini poređenje podataka component-i vrlo jeftinim, omogućavajući da lako utvrdite da li su podaci promenjeni ili ne. Više o tome kako React odlučuje kada da ponovo renderuje component-u možete saznati u referenci za memo
.
Preduzimanje poteza
Sada je vreme da popravimo veliki nedostatak ove igre iks-oks: “O” ne može biti označen na tabli.
Prvi potez će podrazumevano biti “X”. Da bismo to mogli pratiti, dodaćemo još jedan state u component-u Board
:
function Board() {
const [xIsNext, setXIsNext] = useState(true);
const [squares, setSquares] = useState(Array(9).fill(null));
// ...
}
Svaki put kada igrač odigra potez, xIsNext
(boolean) će se promeniti kako bi odredio koji igrač igra sledeći, a state igre će biti sačuvan. Update-ovaćete funkciju handleClick
u component-i Board
kako biste promenili vrednost xIsNext
:
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 (
//...
);
}
Sada, kada kliknete na različite kvadrate, oni će se naizmenično ispunjavati sa X
i O
, kako i treba!
Ali čekajte, postoji problem. Pokušajte da kliknete na isti kvadrat više puta:
X
je prepisan sa O
! Iako bi ovo moglo dodati veoma zanimljiv preokret igri, za sada ćemo se držati originalnih pravila.
Kada označite kvadrat sa X
ili O
, ne proveravate prvo da li kvadrat već ima vrednost X
ili O
. Ovo možete popraviti tako što ćete ranije izaći iz funkcije. Proverićete da li kvadrat već ima vrednost X
ili O
. Ako je kvadrat već popunjen, u funkciji handleClick
vratićete se rano pomoću return
—pre nego što funkcija pokuša da update-uje state table.
function handleClick(i) {
if (squares[i]) {
return;
}
const nextSquares = squares.slice();
//...
}
Sada možete dodavati samo X
ili O
na prazne kvadrate! Ovako bi vaš kod trebalo da izgleda u ovom trenutku:
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> </> ); }
Proglašavanje pobednika
Sada kada igrači mogu naizmenično igrati, želećete da prikažete trenutak kada je igra završena i više nema poteza za igranje. Da biste to uradili, dodaćete pomoćnu funkciju pod nazivom calculateWinner
koja uzima array od 9 kvadrata, proverava da li postoji pobednik i vraća 'X'
, 'O'
ili null
, u zavisnosti od rezultata. Ne brinite previše o funkciji calculateWinner
; ona nije specifična za React:
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;
}
Pozvaćete calculateWinner(squares)
u funkciji handleClick
unutar Board
component-e kako biste proverili da li je neki igrač pobedio. Ovu proveru možete obaviti istovremeno kada proveravate da li je korisnik kliknuo na kvadrat koji već ima X
ili O
. Želeli bismo da se u oba slučaja funkcija završi ranije:
function handleClick(i) {
if (squares[i] || calculateWinner(squares)) {
return;
}
const nextSquares = squares.slice();
//...
}
Da biste obavestili igrače kada je igra završena, možete prikazati tekst poput “Pobednik: X” ili “Pobednik: O”. Da biste to postigli, dodaćete sekciju status
u Board
component-u. Status
će prikazati pobednika ako je igra završena, a ako igra još traje, prikazaće koji igrač je sledeći na potezu:
export default function Board() {
// ...
const winner = calculateWinner(squares);
let status;
if (winner) {
status = "Pobednik: " + winner;
} else {
status = "Sledeći igrač: " + (xIsNext ? "X" : "O");
}
return (
<>
<div className="status">{status}</div>
<div className="board-row">
// ...
)
}
Čestitamo! Sada imate funkcionalnu igru iks-oks. Takođe, upravo ste naučili osnove React-a. Dakle, vi ste ovde pravi pobednik. Evo kako vaš kod treba da izgleda:
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 = 'Pobednik: ' + winner; } else { status = 'Sledeći igrač: ' + (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; }
Dodavanje putovanja kroz vreme
Kao završnu vežbu, omogućićemo “vraćanje u prošlost” na prethodne poteze u igri.
Čuvanje istorije poteza
Ako biste menjali array squares
direktno, implementacija vremenskog putovanja bi bila veoma teška.
Međutim, koristili ste slice()
za kreiranje nove kopije array-a squares
nakon svakog poteza i tretirali ga kao nepromenljiv (immutable). Ovo vam omogućava da sačuvate svaku prethodnu verziju array-a squares
i da se krećete između poteza koji su se već odigrali.
Čuvaćete prošle array-e squares
u drugom array-u nazvanom history
, koji ćete čuvati kao novu varijablu state-a. Array history
predstavlja sve stanje table, od prvog do poslednjeg poteza, i ima strukturu sličnu ovoj:
[
// Pre prvog poteza
[null, null, null, null, null, null, null, null, null],
// Nakon prvog poteza
['X', null, null, null, null, null, null, null, null],
// Nakon drugog poteza
['X', null, null, null, 'O', null, null, null, null],
// ...
]
```jsx
[
// 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'],
// ...
]
Podizanje state-a ponovo
Sada ćete napisati novu component-u na najvišem nivou pod nazivom Game
, kako biste prikazali listu prošlih poteza. Tu ćete smestiti state history
, koji sadrži kompletnu istoriju igre.
Postavljanjem state-a history
u component-u Game
, možete ukloniti state squares
iz njene child component-e Board
. Baš kao što ste “podigli state” iz component-e Square
u component-u Board
, sada ćete ga podići iz component-e Board
u component-u najvišeg nivoa, Game
. Ovo omogućava component-i Game
da ima potpunu kontrolu nad podacima component-e Board
i da joj zadaje uputstva da prikaže prethodne poteze iz history
.
Prvo, dodajte component-u Game
koristeći export default
. Neka renderuje component-u Board
i malo markup-a:
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>
);
}
Napomena: Uklanjate ključne reči export default
ispred deklaracije function Board() {
i dodajete ih ispred deklaracije function Game() {
. Ovo govori vašem fajlu index.js
da koristi component-u Game
kao component-u najvišeg nivoa umesto component-e Board
. Dodatni div
-ovi koje vraća component-a Game
prave prostor za informacije o igri koje ćete kasnije dodati na tablu.
Dodajte state u component-u Game
kako biste pratili koji je sledeći igrač na potezu i istoriju poteza:
export default function Game() {
const [xIsNext, setXIsNext] = useState(true);
const [history, setHistory] = useState([Array(9).fill(null)]);
// ...
Primetite kako je [Array(9).fill(null)]
array sa jednim elementom, koji je sam po sebi array od 9 null
vrednosti.
Da biste prikazali kvadrate za trenutni potez, potrebno je da pročitate poslednji array squares
iz history
. Za ovo vam nije potreban useState
jer već imate dovoljno informacija da ga izračunate tokom renderovanja:
export default function Game() {
const [xIsNext, setXIsNext] = useState(true);
const [history, setHistory] = useState([Array(9).fill(null)]);
const currentSquares = history[history.length - 1];
// ...
Zatim kreirajte funkciju handlePlay
unutar component-e Game
, koju će pozivati component-a Board
kako bi update-ovala igru. Prosledite xIsNext
, currentSquares
i handlePlay
kao props component-i Board
:
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} />
//...
)
}
Hajde da učinimo component-u Board
potpuno kontrolisanom preko props-a koje prima. Izmenite component-u Board
tako da prihvata tri props-a: xIsNext
, squares
i novu funkciju onPlay
, koju Board
može pozvati sa update-ovanim array-om kvadrata kada igrač napravi potez. Zatim uklonite prve dve linije funkcije Board
koje pozivaju useState
:
function Board({ xIsNext, squares, onPlay }) {
function handleClick(i) {
//...
}
// ...
}
Sada zamenite pozive setSquares
i setXIsNext
u funkciji handleClick
unutar component-e Board
jednim pozivom vaše nove funkcije onPlay
, kako bi component-a Game
mogla da update-uje Board
kada korisnik klikne na kvadrat:
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);
}
//...
}
Component-a Board
je potpuno kontrolisana preko props-a koje joj prosleđuje component-a Game
. Potrebno je da implementirate funkciju handlePlay
u component-i Game
kako bi igra ponovo funkcionisala.
Šta bi funkcija handlePlay
trebalo da uradi kada se pozove? Zapamtite da je Board
ranije pozivao setSquares
sa update-ovanim array-om; sada prosleđuje update=ovani array squares
funkciji onPlay
.
Funkcija handlePlay
treba da update-uje state component-e Game
kako bi pokrenula ponovno renderovanje, ali više nemate funkciju setSquares
koju biste mogli da pozovete—sada koristite varijablu state-a history
za čuvanje ovih informacija. Treba da update-ujete history
tako što ćete dodati update-ovani array squares
kao novi unos u istoriji. Takođe treba da promenite vrednost xIsNext
, kao što je Board
ranije radio:
export default function Game() {
//...
function handlePlay(nextSquares) {
setHistory([...history, nextSquares]);
setXIsNext(!xIsNext);
}
//...
}
Ovde, [...history, nextSquares]
kreira novi array koji sadrži sve stavke iz history
, praćene array-om nextSquares
. (Možete čitati ...history
spread sintaksu kao „nabroj sve stavke u history
”.)
Na primer, ako je history
[[null,null,null], ["X",null,null]]
, a nextSquares
je ["X",null,"O"]
, novi array [...history, nextSquares]
biće [[null,null,null], ["X",null,null], ["X",null,"O"]]
.
U ovom trenutku, state je premešten u component-u Game
, i korisnički interfejs bi trebalo da funkcioniše potpuno isto kao i pre refaktorisanja. Evo kako kod treba da izgleda na ovoj tački:
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 = 'Pobednik: ' + winner; } else { status = 'Sledeći igrač: ' + (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; }
Prikazivanje prethodnih poteza
Pošto beležite istoriju igre iks-oks, sada možete prikazati listu prethodnih poteza igraču.
React elementi poput <button>
su regularni JavaScript objekti; možete ih prosleđivati kroz svoju aplikaciju. Da biste prikazali više stavki u React-u, možete koristiti array React elemenata.
Već imate array history
poteza u state-u, pa sada treba da ga transformišete u array React elemenata. U JavaScript-u, za transformaciju jednog array-a u drugi, možete koristiti metodu array map:
[1, 2, 3].map((x) => x * 2) // [2, 4, 6]
Koristićete map
da transformišete svoj history
poteza u React elemente koji predstavljaju dugmad na ekranu, i prikazati listu dugmadi za „skok” na prethodne poteze. Hajde da primenimo map
na history
u component-i Game:
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 = 'Prebacite se na potez #' + move;
} else {
description = 'Prebacite se na početak';
}
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>
);
}
Možete videti kako vaš kod treba da izgleda ispod. Imajte na umu da biste u konzoli trebalo da vidite grešku koja kaže:
Ovu grešku ćete rešiti u sledećem odeljku.
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 = 'Pobednik: ' + winner; } else { status = 'Sledeći igrač: ' + (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 = 'Prebacite se na potez #' + move; } else { description = 'Prebacite se na početak'; } 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; }
Dok iterirate kroz array history
unutar funkcije koju ste prosledili map
, argument squares
prolazi kroz svaki element array-a history
, a argument move
prolazi kroz svaki indeks array-a: 0
, 1
, 2
, …. (U većini slučajeva trebaju vam stvarni elementi array-a, ali za prikaz liste poteza biće vam potrebni samo indeksi.)
Za svaki potez u istoriji igre iks-oks, kreirate stavku liste <li>
koja sadrži dugme <button>
. Dugme ima onClick
handler koji poziva funkciju jumpTo
(koju još niste implementirali).
Za sada, trebalo bi da vidite listu poteza koji su se dogodili u igri i grešku u konzoli. Hajde da razgovaramo o značenju greške „key”.
Odabir ključa
Kada renderujete listu, React čuva neke informacije o svakoj renderovanoj stavci liste. Kada update-ujete listu, React treba da odredi šta se promenilo. Mogli ste da dodate, uklonite, preuredite ili update-ujete stavke liste.
Zamislite prelazak sa:
<li>Alexa: 7 zadataka preostalo</li>
<li>Ben: 5 zadataka preostalo</li>
to
<li>Ben: 9 zadataka preostalo</li>
<li>Claudia: 8 zadataka preostalo</li>
<li>Alexa: 5 zadataka preostalo</li>
Pored update-ovanih brojeva, osoba koja čita ovo verovatno bi rekla da ste zamenili redosled Alexe i Bena i umetnuli Claudiu između Alexe i Bena. Međutim, React je računarski program i ne zna šta ste želeli da uradite, pa je potrebno da za svaku stavku liste navedete svojstvo key kako biste razlikovali svaku stavku od njenih „sestrinskih” stavki. Ako su vaši podaci iz baze podataka, ID-ovi Alexe, Bena i Claudie iz te baze mogli bi da se koriste kao ključevi.
<li key={user.id}>
{user.name}: {user.taskCount} zadataka preostalo
</li>
Kada se lista ponovo renderuje, React uzima ključ svake stavke liste i pretražuje stavke prethodne liste tražeći ključ koji se poklapa. Ako trenutna lista ima ključ koji nije postojao ranije, React kreira component-u. Ako trenutnoj listi nedostaje ključ koji je postojao u prethodnoj listi, React poništava prethodnu component-u. Ako se dva ključa poklapaju, odgovarajuća component-a se premesti.
Ključevi govore React-u o identitetu svake component-e, što omogućava React-u da očuva state između ponovnih renderovanja. Ako se ključ component-e promeni, component-a će biti poništena i ponovo kreirana sa novim state-om.
key
je specijalan i rezervisan property u React-u. Kada se element kreira, React izvlači property key
i čuva ga direktno na vraćenom elementu. Iako key
može izgledati kao da se prosleđuje kao props, React automatski koristi key
da odluči koje component-e da update-uje. Ne postoji način da component-a pita koji je key
njen parent odredio.
Snažno se preporučuje da dodelite odgovarajuće ključeve svaki put kada pravite dinamičke liste. Ako nemate odgovarajući ključ, možda bi trebalo da razmislite o restrukturiranju vaših podataka kako biste ga obezbedili.
Ako nije naveden ključ, React će prijaviti grešku i koristiti indeks array-a kao ključ podrazumevano. Korišćenje indeksa array-a kao ključa je problematično kada pokušavate da promenite redosled stavki liste ili da umetnete/uklonite stavke liste. Eksplicitno prosleđivanje key={i}
utišava grešku, ali ima iste probleme kao i korišćenje indeksa array-a i ne preporučuje se u većini slučajeva.
Ključevi ne moraju biti globalno jedinstveni; moraju biti jedinstveni samo između component-i i njihovih „sestrinskih” component-i.
Implementacija putovanja kroz vreme
U istoriji igre iks-oks, svaki prethodni potez ima jedinstveni ID povezan sa njim: to je redni broj poteza. Potezi se nikada neće preuređivati, brisati ili umetati u sredinu, pa je bezbedno koristiti indeks poteza kao ključ.
U funkciji Game
, možete dodati ključ kao <li key={move}>
, i ako ponovo učitate renderovanu igru, React-ova greška „key” bi trebalo da nestane:
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 = 'Pobednik: ' + winner; } else { status = 'Sledeći igrač: ' + (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 = 'Prebacite se na potez #' + move; } else { description = 'Prebacite se na početak'; } 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; }
Pre nego što implementirate jumpTo
, potrebno je da component-a Game
prati koji korak korisnik trenutno gleda. Da biste to uradili, definišite novu state varijablu pod nazivom currentMove
, sa podrazumevanom vrednošću 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];
//...
}
Zatim, update-ujte funkciju jumpTo
unutar component-e Game
tako da update-uje currentMove
. Takođe ćete postaviti xIsNext
na true
ako je broj na koji menjate currentMove
paran.
export default function Game() {
// ...
function jumpTo(nextMove) {
setCurrentMove(nextMove);
setXIsNext(nextMove % 2 === 0);
}
//...
}
Sada ćete napraviti dve izmene u funkciji handlePlay
component-e Game
, koja se poziva kada kliknete na kvadrat.
- Ako se “vratite u prošlost” i napravite novi potez od te tačke, želite da zadržite istoriju samo do te tačke. Umesto da dodate
nextSquares
posle svih stavki (...
spread sintaksa) uhistory
, dodaćete ga posle svih stavki uhistory.slice(0, currentMove + 1)
kako biste zadržali samo taj deo stare istorije. - Svaki put kada se napravi potez, potrebno je da update-ujete
currentMove
kako bi ukazivao na najnoviji unos u istoriji.
function handlePlay(nextSquares) {
const nextHistory = [...history.slice(0, currentMove + 1), nextSquares];
setHistory(nextHistory);
setCurrentMove(nextHistory.length - 1);
setXIsNext(!xIsNext);
}
Na kraju, izmenićete component-u Game
tako da renderuje trenutno odabrani potez, umesto da uvek renderuje poslednji potez:
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];
// ...
}
Ako kliknete na bilo koji korak u istoriji igre, tabla za iks-oks bi trebalo odmah da se update-uje i prikaže kako je izgledala nakon tog poteza.
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 = 'Pobednik: ' + winner; } else { status = 'Sledeći igrač: ' + (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 = 'Prebacite se na potez #' + move; } else { description = 'Prebacite se na početak'; } 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; }
Finalno sređivanje
Ako pažljivo pogledate kod, primetićete da je xIsNext === true
kada je currentMove
paran i xIsNext === false
kada je currentMove
neparan. Drugim rečima, ako znate vrednost currentMove
, uvek možete odrediti šta treba da bude xIsNext
.
Nema potrebe da čuvate obe ove vrednosti u state-u. Zapravo, uvek pokušajte da izbegavate redundantni state. Pojednostavljivanje onoga što čuvate u state-u smanjuje broj grešaka i čini vaš kod lakšim za razumevanje. Izmenite component-u Game
tako da ne čuva xIsNext
kao zasebnu state varijablu, već da ga izračunava na osnovu vrednosti 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);
}
// ...
}
Više vam nije potrebna deklaracija state varijable xIsNext
niti pozivi funkcije setXIsNext
. Sada ne postoji mogućnost da xIsNext
bude van sinhronizacije sa currentMove
, čak i ako napravite grešku dok kodirate component-e.
Završetak
Čestitamo! Napravili ste igru iks-oks koja:
- Omogućava igranje iks-oks,
- Indikuje kada je igrač pobedio u igri,
- Čuva istoriju igre kako se igra odvija,
- Omogućava igračima da pregledaju istoriju igre i vide prethodne verzije table.
Odličan posao! Nadamo se da sada imate solidno razumevanje kako React funkcioniše.
Pogledajte konačni rezultat ovde:
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 = 'Pobednik: ' + winner; } else { status = 'Sledeći igrač: ' + (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 = 'Prebacite se na potez #' + move; } else { description = 'Prebacite se na početak'; } 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; }
Ako imate dodatno vreme ili želite da vežbate svoje nove React veštine, evo nekoliko ideja za unapređenja igre iks-oks, poređanih po rastućem nivou težine:
- Samo za trenutni potez prikažite poruku “Nalazite se na potezu #…” umesto dugmeta.
- Prepišite component-u
Board
da koristi dve petlje za kreiranje kvadrata umesto da ih hardkodirate. - Dodajte dugme za prebacivanje koje omogućava sortiranje poteza u rastućem ili opadajućem redosledu.
- Kada neko pobedi, istaknite tri kvadrata koja su dovela do pobede (a kada niko ne pobedi, prikažite poruku o nerešenom rezultatu).
- Prikazujte lokaciju svakog poteza u formatu (red, kolona) na listi istorije poteza.
Kroz ovaj tutorijal ste obradili React koncepte uključujući elemente, component-e, props i state. Sada kada ste videli kako ovi koncepti funkcionišu pri izradi igre, pogledajte Razmišljanje u React-u da biste videli kako isti React koncepti funkcionišu pri izradi korisničkog interfejsa aplikacije.