Tutorial: Introducció a React
Aquest tutorial no assumeix cap coneixement previ sobre React.
Abans de començar el tutorial
Construirem un petit joc durant aquest tutorial. Podries estar temptat a obviar-lo perquè tu no estàs construint jocs al dia a dia, però dóna-li una oportunitat. Les tècniques que aprendràs al tutorial són fonamentals per a construir qualsevol aplicació de React, i dominar-les et donarà una comprensió profunda de React.
Consell
Aquest tutorial està dissenyat per a persones que prefereixen aprendre fent. Si tu prefereixes aprendre els conceptes des del principi, revisa la nostra guia pas a pas. Podries trobar aquest tutorial i la guia, complementàries entre si.
Aquest tutorial està dividit en diverses seccions:
- Configuració per al tutorial et donarà un punt de partida per seguir el tutorial.
- Visió general t’ensenyarà els fonaments de React: components, props i estat.
- Completant el joc t’ensenyarà les tècniques més comunes en desenvolupament de React.
- Afegint viatge a través del temps et donarà una visió més profunda de les fortaleses úniques de React.
No has de completar totes les seccions alhora per a poder treure partit del tutorial. Prova d’arribar tan lluny com puguis, fins i tot si només són una o dues seccions.
Què estem construint?
En aquest tutorial, et mostrarem com construir un joc de tres en ratlla interactiu amb React.
Pots veure el que construirem aquí: Resultat Final. Si creus que el codi no té sentit, o si no estàs familiaritzat amb la sintaxi de codi, no et preocupis! L’objectiu d’aquest tutorial és ajudar-te a entendre React i la seva sintaxi.
Recomanem que revisis el joc de tres en ratlla abans de continuar amb el tutorial. Una de les característiques que notaràs és que hi ha una llista enumerada a la dreta del tauler del jugador. Aquesta llista dóna un historial de tots els moviments que han ocorregut en el joc, i es va actualitzant a mesura que el joc progressa.
Pots tancar el joc de tres en ratlla una vegada que et familiaritzis amb ell. Començarem des d’una plantilla més simple en aquest tutorial. El següent pas és preparar-te perquè puguis començar a construir el joc.
Prerequisits
Assumim que tens certa familiaritat amb HTML i JavaScript, de totes maneres hauries de ser capaç d’entendre-ho tot fins i tot si véns d’un llenguatge de programació diferent. També suposem que estàs familiaritzat amb conceptes de programació com a funcions, objectes, arrays, i en menor mesura, classes.
Si necessites revisar JavaScript, et recomanem llegir aquesta guia. Tingues en compte que també fem servir algunes característiques d’ES6, una versió recent de JavaScript. En aquest tutorial, estem fent servir funcions fletxa, classes, sentències let
i const
. Pots fer servir Babel REPL per revisar a quin codi compila ES6.
Configuració per al tutorial
Hi ha dues maneres de completar aquest tutorial: pots escriure el codi al teu navegador, o pots configurar el teu entorn de desenvolupament local en el teu ordinador.
Opció 1: Escriu codi al navegador
Aquesta és la manera més ràpida de començar!
Primer, obre aquest codi inicial en una nova pestanya. La nova pestanya haurà de mostrar un tauler buit del joc de tres en ratlla i codi de React. Estarem editant el codi de React en aquest tutorial.
Ara pots saltar a la segona opció de configuració o anar a la secció de visió general per obtenir una idea general de React.
Opció 2: Entorn de desenvolupament local
Aquesta és completament opcional i no és requerida per a aquest tutorial!
Opcional: Instruccions per seguir endavant localment utilitzant el teu editor de text preferit
Aquesta configuració requereix més feina però et permet completar el tutorial usant un editor de la teva elecció. Aquí els passos a seguir:
- Assegura’t de tenir una versió recent de NODE.JS instal·lada.
- Segueix les instruccions d’instal·lació de Create React App per fer un nou projecte.
npx create-react-app mi-app
- Elimina tots els arxius a la carpeta
src/
del nou projecte.
Nota:
No eliminis la carpeta
src
per complet, només els arxius de codi font originals dins d’ella. Reemplaçarem els arxius de codi font per defecte amb exemples per a aquest projecte en el següent pas.
cd my-app
cd src
# Si fas servir Mac o Linux:
rm -f *
# O, si fas servir Windows:
del *
# Després, torna a la carpeta del projecte
cd ..
- Afegeix un fitxer anomenat
index.css
a la carpetasrc/
amb aquest codi CSS. - Afegeix un fitxer anomenat
index.js
a la carpetasrc/
amb aquest codi JS. - Afegeix aquestes 3 línies a la part superior de l’arxiu
index.js
a la carpetasrc/
:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
Ara, si executes npm start
a la carpeta del projecte i obres http://localhost:3000
al navegador, hauries de veure un camp de tres en ratlla buit.
Recomanem seguir aquestes instruccions per configurar el ressaltat de sintaxi per al teu editor.
Ajuda, estic encallat!
Si t’encalles, revisa els recursos de suport de la comunitat. En particular, Reactiflux Xat és una gran manera d’obtenir ajuda ràpidament. Si no reps una resposta, o segueixes encallat, si us plau crea un issue, i t’ajudarem.
Visió general
Ara que està el teu entorn configurat, t’ensenyarem una visió general de React!
Què és React?
React és una llibreria de JavaScript declarativa, eficient i flexible per construir interfícies d’usuari. Permet compondre interfícies complexes de petites i aïllades peces de codi anomenades “components”.
React té pocs tipus diferents de components, però comencem amb la subclasse React.Component
:
class ShoppingList extends React.Component {
render() {
return (
<div className="shopping-list">
<h1>Llista de compres per {this.props.name}</h1>
<ul>
<li>Instagram</li>
<li>WhatsApp</li>
<li>Oculus</li>
</ul>
</div>
);
}
}
// Ús d'exemple: <ShoppingList name="Mark" />
Aviat pararem atenció a les etiquetes que semblen XML. Fem servir components per dir-li a React el que volem que es vegi a la pantalla. Quan les nostres dades canviïn, React actualitzarà eficientment i tornarà a renderitzar els nostres components.
Aquí, ShoppingList és una classe de component de React, o tipus de component de React. Un component accepta paràmetres, anomenats props
(abreviatura de “propietats”), i retorna una jerarquia de vistes a mostrar a través del mètode render
.
El mètode render
retorna una descripció del que vols veure a la pantalla. React pren la descripció i mostra el resultat. En particular, render
retorna un element de React, el qual és una lleugera descripció del que cal renderitzar. La majoria de desenvolupadors de React usen una sintaxi especial anomenada “JSX” que facilita l’escriptura d’aquestes estructures. La sintaxi <div />
és transformada en temps de construcció (build time) a React.createElement('div')
. L’exemple anterior és equivalent a:
return React.createElement('div', {className: 'shopping-list'},
React.createElement('h1', /* ... h1 children ... */),
React.createElement('ul', /* ... ul children ... */)
);
Veure la versió completa estesa.
Si tens curiositat, createElement()
està descrit en més detall a la referència de l’API, però no ho farem servir en aquest tutorial. En canvi, seguirem fent servir JSX.
JSX ve amb tot el poder de JavaScript. Pots posar qualsevol expressió de JavaScript en l’interior de les claus dins de JSX. Cada element de React és un objecte de JavaScript que pots emmagatzemar en una variable o passar al voltant del teu programa.
El component anterior ShoppingList
només renderitza components preconstruïts del DOM com <div />
i <li />
. Però, també pots compondre i renderitzar components personalitzats de React. Per exemple, ara podem referir-nos al llistat complet de compres escrivint <ShoppingList />
. Cada compoent de React està encapsulat i pot operar independentment; això et permet construir interfícies complexes des de components simples.
Inspeccionant el codi inicial
Si treballes amb el tutorial al navegador, obre aquest codi en una nova pestanya: Codi inicial. Si treballes localment, obre src/index.js
a la carpeta del teu projecte (ja has tocat aquest arxiu durant la configuració).
Aquest codi inicial és la base del que estàs construint. Ens han proveït els estils de CSS així que només necessites enfocar-te a aprendre React i programar el joc tres en ratlla.
Inspeccionant el codi, notaràs que tenim 3 components de React:
- Square
- Board
- Game
El component Square renderitza un simple <button>
i el Board renderitza 9 quadrats. El component Game renderitza una taula amb valors de posició per defecte que modificarem després. Actualment no hi ha components interactius.
Passant dades a través de props
Només per embrutar-nos les mans, passarem algunes dades del nostre component Board al nostre component Square.
Recomanem fermament escriure el codi a mà mentre segueixes el tutorial sense copiar i enganxar. Això t’ajudarà a desenvolupar una memòria muscular i una entesa més sòlida.
Al mètode renderSquare
de Board, canvia el codi per a passar una prop anomenada value
a Square:
class Board extends React.Component {
renderSquare(i) {
return <Square value={i} />; }
}
Canvia el mètode render
de Square per mostrar aquest valor, reemplaçant {/* TODO */}
amb {this.props.value}
:
class Square extends React.Component {
render() {
return (
<button className="square">
{this.props.value} </button>
);
}
}
abans:
Després: Hauries de veure un número a cada quadrat del resultat renderitzat.
Veure el codi complet en aquest punt
Felicitats! Acabes de “passar una prop” d’un component pare Board a un component fill Square. Passant props és com la informació flueix en apps de React, de pares a fills.
Fent un component interactiu
Omplirem el component Square amb una “X” quan li fem clic.
Primer, canviarem l’etiqueta button que és retornada del mètode render()
del component Square pel següent codi:
class Square extends React.Component {
render() {
return (
<button className="square" onClick={function() {alert('clic'); }}> {this.props.value}
</button>
);
}
}
Si fas clic a un quadrat ara, hauries de veure un avís al teu navegador.
Nota
Per continuar escrivint codi sense problemes i evitar el confús comportament de
this
, farem servir la sintaxi de funcions fletxa per manejar esdeveniments aquí i més avall:class Square extends React.Component { render() { return ( <button className="square" onClick={() => alert('clic')}> {this.props.value} </button> ); } }
Tingues en compte com amb
onClick={() => alert('clic')}
, estem passant una funció com a valor de la proponClick
. React només cridarà aquesta funció després d’un clic. Oblidar() =>
i escriureonClick={alert('clic')}
és un error comú, i executaria l’alerta cada vegada que el component es rerenderitzi.
Com un següent pas, volem que el component Square “recordi” que va ser clicat, i s’ompli amb una “X”. Per “recordar” coses, els components fan servir l’estat.
Els components de React poden tenir estat establint this.state
als seus constructors. this.state
ha de ser considerat com privat pel component de React en que és definit. Emmagatzemarem el valor actual d’un quadrat a this.state
, i el canviarem quan el quadrat sigui clicat.
Primer, afegim el constructor a la classe per inicialitzar l’estat:
class Square extends React.Component {
constructor(props) { super(props); this.state = { value: null, }; }
render() {
return (
<button className="square" onClick={() => alert('click')}>
{this.props.value}
</button>
);
}
}
Nota
A les classes de JavaScript, necessites sempre cridar
super
quan defineixes el constructor d’una subclasse. Totes les classes de components de React que tenen unconstructor
han de començar amb una crida asuper(props)
.
Ara canviarem el mètode render
de Square per mostrar el valor de l’estat actual quan és clicat:
- Canvia
this.props.value
perthis.state.value
dins de l’etiqueta<button>
. - Canvia el gestor d’esdeveniment
onClick={...}
peronClick={() => this.setState ({value: 'X'})}
. - Posa les props
className
ionClick
en línies separades per millor llegibilitat.
Després d’aquests canvis, l’etiqueta <button>
que és retornada del mètode render
de Square es veu així:
class Square extends React.Component {
constructor(props) {
super(props);
this.state = {
value: null,
};
}
render() {
return (
<button
className="square" onClick={() => this.setState({value: 'X'})} >
{this.state.value} </button>
);
}
}
Cridant a this.setState
des del gestor onClick
al mètode render
de Square, diem a React que rerenderitzi el quadrat sempre que la seva <button>
és clicada. Després de l’actualització, el this.state.value
del quadrat serà 'X'
, així que veurem X
al tauler de joc. Si tu fas clic a qualsevol quadrat, una X
s’hauria de mostrar al mateix.
Quan crides setState
a un component, React actualitza automàticament els components fills dins el mateix també.
Veure el codi complet en aquest punt
Eines de desenvolupament
L’extensió de React DevTools Library per Chrome i Firefox et permet inspeccionar l’arbre de components de React amb les teves eines de desenvolupament del navegador.

React DevTools et permet revisar les props i l’estat dels teus components de React.
Després d’instal·lar React DevTools, pots fer clic dret a qualsevol element de la pàgina, clic a “Inspeccionar element” per obrir les eines de desenvolupament, i la pestanyes de React (“⚛️ Components” and “⚛️ Profiler”) apareixeran com al final a la dreta. Utilitza “⚛️ Components” per inspeccionar components del arbre.
No obstant això, notar que hi ha uns quants passos extres per fer-lo funcionar amb CodePen:
- Entra o registra’t i confirma el teu correu electrònic (requerit per prevenir spam).
- Clic al botó “Fork”.
- Click a “Change View” i després selecciona “Debug mode”.
- A la nova pestanya que s’obre, les DevTools ara haurien de tenir una pestanya de React.
Completant el joc
Ara tenim els blocs de construcció bàsics per al nostre joc de tres en ratlla. Per completar el joc, necessitem alternar col·locant “X” i “O” al tauler, i necessites una manera de determinar el guanyador.
Elevant l’estat
Actualment, cada component Square manté l’estat del joc. Per determinar un guanyador, necessitem mantenir el valor de cada un dels 9 quadrats a un sol lloc.
Podríem pensar que el tauler simplement hauria de preguntar a cada quadrat pel seu estat. Encara que aquest enfocament és possible en React, t’incentivem a què no ho facis servir perquè el codi es torna difícil d’entendre, susceptible a errors, i difícil de refactorizar. Enlloc seu, el millor enfocament és emmagatzemar l’estat del joc al component pare Board en comptes de cada component Square. El component Board pot dir-li a cada quadrat què mostrar passant-li un prop tal com hem fet quan hem passat un número a cada quadrat.
Per recopilar dades de múltiples fills, o tenir dos components fills comunicats entre si, necessites declarar l’estat compartit al seu component pare. El component pare pot passar l’estat cap als fills usant props; això manté els components fills sincronitzats entre ells i amb el seu component pare.
Elevar l’estat al component pare és comú quan components de React són refactoritzats, fem servir aquesta oportunitat per intentar-ho.
Afegeix un constructor al Board i estableix l’estat inicial de Board per contenir un array amb 9 valors null. Aquests 9 nulls corresponen als 9 quadrats:
class Board extends React.Component {
constructor(props) { super(props); this.state = { squares: Array(9).fill(null), }; }
renderSquare(i) {
return <Square value={i} />;
}
Després, quan omplim el tauler, l’array this.state.squares
tindrà el següent aspecte:
[
'O', null, 'X',
'X', 'X', 'O',
'O', null, null,
]
El mètode renderSquare
del component Board actualment es veu així:
renderSquare(i) {
return <Square value={i} />;
}
Al principi, passem el prop value
des del Board per mostrar els números de 0 a 8 a cada quadrat. En un pas previ, reemplacem els números amb una marca “X” determinat per l’estat del propi Square. Això és perquè el quadrat actualment ignora el prop value
passat pel Board.
Ara farem servir el prop passant el mecanisme altra vegada. Modificarem el Board per instruir cada Square sobre el seu valor actual ('X'
,'O'
, o null
). Ja tenim definit l’array squares
al constructor del Board, i modificarem el mètoderenderSquare
perquè el llegeixi des d’allà:
renderSquare(i) {
return <Square value={this.state.squares[i]} />; }
Veure el codi complet en aquest punt
Cada Square ara rebrà un prop value
que serà 'X'
, 'O'
, o null
per quadrats buits.
Després, ens cal canviar el que passa quan un quadrat és clicat. El component Board ara manté quins quadrats estan omplerts. Necessitem crear un mètode perquè el quadrat actualitzi l’estat del component Board. A causa que l’estat és considerat privat al component que el defineix, no podem actualitzar l’estat de Board directament des de Square.
En canvi, passarem una funció com prop des del Board a Square i farem que Square cridi a aquesta funció quan un quadrat sigui clicat. Canviarem el mètode renderSquare
a Board a:
renderSquare(i) {
return (
<Square
value={this.state.squares[i]}
onClick={() => this.handleClick(i)} />
);
}
Nota
Dividim l’element retornat en múltiples línies per llegibilitat, i afegim parèntesi perquè JavaScript no insereixi un punt i coma després del
return
i trenqui el nostre codi.
Ara estem passant dues props des Board a Square: value
i onClick
. El prop onClick
és una funció que Square pot cridar quan sigui clicat. Farem els següents canvis a Square:
- Substituir
this.state.value
perthis.props.value
al mètoderender
de Square - Substituir
this.setState()
perthis.props.onClick()
al mètoderender
de Square - Eliminar el
constructor
de Square perquè el component ja no faci el seguiment de l’estat del joc
Després d’aquests canvis, el component Square té aquesta pinta:
class Square extends React.Component { render() { return (
<button
className="square"
onClick={() => this.props.onClick()} >
{this.props.value} </button>
);
}
}
Quan un quadrat és clicat, la funció onClick
proveïda pel component Board és cridada. Fem un repàs de com hem aconseguit això:
- El prop
onClick
al component preconstruït del DOM<button>
li diu a React per establir un escoltador de l’esdeveniment clic. - Quan el botó és clicat, React trucarà al gestor d’esdeveniment
onClick
que està definit al mètoderender()
de Square. - Aquest gestor d’esdeveniment crida a
this.props.onClick()
. El proponClick
del component Square va ser especificat pel component Board. - Com que el Board va passar
onClick={() => this.handleClick(i)}
a Square, el component Square crida athis.handleClick(i)
quan és clicat. - No tenim definit el mètode
handleClick()
, així que el nostre codi falla. Si fas clic ara veuràs una pantalla vermella d’error que diu alguna cosa com “this.handleClick is not a function” (this.handleClick no és una funció).
Nota
L’atribut
onClick
de l’element<button>
del DOM té un significat especial per a React perquè és un component preconstruït. Per components personalitzats com Square, la nomenclatura la decideixes tu. Podríem donar-li qualsevol nom al proponClick
de Square o al mètodehandleClick
de Board, i el codi funcionaria de la mateixa forma. En React, però, és una convenció usar els nomson[Event]
per props que representen esdeveniments ihandle[Event]
per als mètodes que manegen els esdeveniments.
Quan intentem clicar un quadrat, hauríem d’obtenir un error perquè no hem definit handleClick
encara. Ara afegirem handleClick
a la classe Board:
class Board extends React.Component {
constructor(props) {
super(props);
this.state = {
squares: Array(9).fill(null),
};
}
handleClick(i) { const squares = this.state.squares.slice(); squares[i] = 'X'; this.setState({squares: squares}); }
renderSquare(i) {
return (
<Square
value={this.state.squares[i]}
onClick={() => this.handleClick(i)}
/>
);
}
render() {
const status = 'Next player: X';
return (
<div>
<div className="status">{status}</div>
<div className="board-row">
{this.renderSquare(0)}
{this.renderSquare(1)}
{this.renderSquare(2)}
</div>
<div className="board-row">
{this.renderSquare(3)}
{this.renderSquare(4)}
{this.renderSquare(5)}
</div>
<div className="board-row">
{this.renderSquare(6)}
{this.renderSquare(7)}
{this.renderSquare(8)}
</div>
</div>
);
}
}
Veure el codi complet en aquest punt
Després d’aquests canvis, podem clicar novament els quadrats per omplir-los de la mateixa manera que ho hem fet abans. No obstant això, ara l’estat està emmagatzemat al component Board en lloc de cada component Square. Quan l’estat del Board canvia, els components Square es rerenderitzen automàticament. Mantenir l’estat de tots els quadrats al component Board ens permetrà determinar el guanyador al futur.
A causa que el component Square ara no manté estat, els components Square reben valors del component Board i informen al mateix quan són clicats. En termes de React, els components Square ara són components controlats. El component Board té control complet sobre ells.
Notar com a handleClick
, cridem .slice()
per crear una còpia de l’array de squares
per modificar-lo en comptes de modificar l’array existent. Ara explicarem per què crear una còpia de l’array squares
en la següent secció.
Per què és important la immutabilitat?
A l’exemple de codi anterior, hem suggerit que usessis el mètode .slice()
per crear una còpia de l’array de squares
per modificar-lo en comptes de modificar l’array existent. Ara discutirem la immutabilitat i per què és important aprendre-la.
Hi ha generalment dos enfocaments per canviar dades. El primer enfocament és mutar les dades directament canviant els seus valors. El segon enfocament és reemplaçar les dades amb una nova còpia que té els canvis desitjats.
Canvi de dades amb mutació
var player = {score: 1, name: 'Jeff'};
player.score = 2;
// Ara `player` és {score: 2, name: 'Jeff'}
Canvi de dades sense mutació
var player = {score: 1, name: 'Jeff'};
var newPlayer = Object.assign({}, player, {score: 2});
// Ara `player` no ha canviat, però` newPlayer` és {score: 2, name: 'Jeff'}
// O si fas servir la sintaxi proposada de propagació d'objecte, pots escriure:
// var newPlayer={... player, score: 2};
El resultat final és el mateix, però com que no mutem (o canviem les dades subjacents) directament, obtenim molts beneficis descrits a continuació.
Funcionalitats complexes es tornen simples
La immutabilitat fa que funcionalitats complexes siguin molt més fàcil d’implementar. Després en aquest tutorial, implementarem una funcionalitat de “viatge a través del temps” que ens permet repassar l’historial del joc tres en ratlla i “tornar” a moviments previs. Aquesta funcionalitat no és específica de jocs, una habilitat de desfer i refer certes accions és un requeriment comú en aplicacions. Evitar la mutació de dades directament ens permet mantenir intactes versions prèvies de l’historial del joc, i reusar després.
Detectar canvis
Detectar canvis en objectes mutables és difícil perquè són modificats directament. Aquesta detecció requereix que els objectes mutables siguin comparats a la còpia prèvia del mateix i que l’arbre sencer de l’objecte sigui recorregut.
Detectar canvis en objectes immutables és considerablement més senzill. Si l’objecte immutable que està sent referenciat és diferent de l’anterior, vol dir que l’objecte ha canviat.
Determinar quan rerenderitzar en React
El benefici principal d’immutabilitat és que t’ajuda a construir components purs a React. Dades immutables poden determinar fàcilment si s’han realitzat canvis, que ajuda també a determinar quan un component requereix ser rerenderitzat.
Pots aprendre més sobre shouldComponentUpdate()
i com pots construir components purs llegint Optimitzant el rendiment.
Funcions com a components
Ara canviarem el component Square a ser un funció com a component.
A React, funcions com a components són una forma més simple d’escriure components que només contenen un mètode render
i no tenen estat propi. En lloc de definir una classe que extén React.Component
, podem escriure una funció que pren props
com a paràmetres i retorna el que s’ha de renderitzar. Funcions com a components són menys tediosos d’escriure que classes, i molts components poden ser expressats d’aquesta manera.
Reemplaça la classe Square per aquesta funció:
function Square(props) {
return (
<button className="square" onClick={props.onClick}>
{props.value}
</button>
);
}
Hem canviat this.props
per props
les dues vegades que apareix.
Veure el codi complet en aquest punt
Nota
Quan modifiquem el component Square a ser un component de funció, també hem canviat
onClick={() => this.props.onClick()}
a una més curtaonClick={props.onClick}
(notar la manca de parèntesi en amdós costats).
Prenent torns
Ara ens cal corregir un defecte obvi en el nostre joc tres en ratlla: les “O” no poden ser marcades al tauler.
Establirem que el primer moviment sigui una “X” per defecte. Podem establir el valor per defecte modificant l’estat inicial en el nostre constructor del component Board:
class Board extends React.Component {
constructor(props) {
super(props);
this.state = {
squares: Array(9).fill(null),
xIsNext: true, };
}
Cada vegada que el jugador faci un moviment, xIsNext
(un booleà) serà invertit per determinar quin jugador segueix i l’estat del joc serà desat. Actualitzarem la funció handleClick
del component Board per invertir el valor de xIsNext
:
handleClick(i) {
const squares = this.state.squares.slice();
squares[i] = this.state.xIsNext ? 'X' : 'O'; this.setState({
squares: squares,
xIsNext: !this.state.xIsNext, });
}
Amb aquest canvi, “X”s i “O”s poden prendre torns. Intenta-ho!
També canviarem el text de “status” al render
del Board perquè mostri quin jugador té el següent torn:
render() {
const status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
return (
// la resta no ha canviat
Després d’aplicar aquests canvis, hauríem de tenir aquest component Board:
class Board extends React.Component {
constructor(props) {
super(props);
this.state = {
squares: Array(9).fill(null),
xIsNext: true, };
}
handleClick(i) {
const squares = this.state.squares.slice(); squares[i] = this.state.xIsNext ? 'X' : 'O'; this.setState({ squares: squares, xIsNext: !this.state.xIsNext, }); }
renderSquare(i) {
return (
<Square
value={this.state.squares[i]}
onClick={() => this.handleClick(i)}
/>
);
}
render() {
const status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
return (
<div>
<div className="status">{status}</div>
<div className="board-row">
{this.renderSquare(0)}
{this.renderSquare(1)}
{this.renderSquare(2)}
</div>
<div className="board-row">
{this.renderSquare(3)}
{this.renderSquare(4)}
{this.renderSquare(5)}
</div>
<div className="board-row">
{this.renderSquare(6)}
{this.renderSquare(7)}
{this.renderSquare(8)}
</div>
</div>
);
}
}
Veure el codi complert en aquest punt
Declarant un guanyador
Ara que hem mostrat quin jugador té el següent torn, també hem de mostrar quan algú ha guanyat el joc i si no hi ha més moviments a fer. Copia aquesta funció de suport i enganxa-la al final de l’arxiu.
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;
}
Donat un array de 9 quadrats, aquesta funció comprovarà si hi ha un guanyador i retornarà 'X'
, 'O'
o null
segons correspongui.
Cridarem a calculateWinner (squares)
al mètode render
del component Board per revisar si un jugador ha guanyat. Si un jugador ha guanyat, podem mostrar un text com: “Winner: X” o “Winner: O”. Reemplaçarem la declaració de l’status
al mètode render
del Board amb aquest codi:
render() {
const winner = calculateWinner(this.state.squares); let status; if (winner) { status = 'Winner: ' + winner; } else { status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O'); }
return (
// la resta del codi no ha canviat
Ara podem canviar la funció handleClick
del component Board per retornar ràpidament ignorant un clic si algú ha guanyat el joc o si un quadrat està ja emplenat:
handleClick(i) {
const squares = this.state.squares.slice();
if (calculateWinner(squares) || squares[i]) { return; } squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({
squares: squares,
xIsNext: !this.state.xIsNext,
});
}
Veure el codi complet en aquest punt
Felicitats! Ara tens un joc tres en ratlla funcionant. I també acabes d’aprendre la base de React. Així que ets probablement el veritable guanyador aquí.
Afegint viatge a través del temps
Com a exercici final, farem possible “retrocedir a través del temps” al moviment previ del joc.
Emmagatzemar un historial de moviments
Si mutéssim l’array de squares
, implementar viatge a través del temps seria molt difícil.
No obstant això, fem servir slice()
per crear una còpia nova de l’array de squares
després de cada moviment, i el tractem com immutable. Això ens permet emmagatzemar cada versió prèvia de l’array de squares
, i navegar entre els torns que ja han passat.
Emmagatzemarem els passats arrays de squares
a un altre array anomenat history
. La matriu history
representa tots els estats del tauler, des del primer moviment fins a l’últim, i té una forma com aquesta:
history = [
// Abans del primer moviment
{
squares: [
null, null, null,
null, null, null,
null, null, null,
]
},
// Després del primer moviment
{
squares: [
null, null, null,
null, 'X', null,
null, null, null,
]
},
// Després del segon moviment
{
squares: [
null, null, null,
null, 'X', null,
null, null, 'O',
]
},
// ...
]
Ara ens cal decidir quin component ha de ser el propietari de l’estat history
.
Elevant l’estat, una altra vegada
Volem que el component de nivell superior, Game, mostri una llista dels moviments passats. Us cal accés a l’historial per fer-ho, així que col·locarem l’estat history
al component Game.
Col·locant l’estat history
al component Game et permet eliminar l’estat squares
del seu component fill Board. Tal com hem “elevat l’estat” del component Square al component Board, ara elevarem del Board al component Game. Això donarà al component Game complet control sobre les dades de Board, i permetrà instruir el tauler que renderitzi els torns previs des del history
.
Primer, establim l’estat inicial per al component Game al seu constructor:
class Game extends React.Component {
constructor(props) { super(props); this.state = { history: [{ squares: Array(9).fill(null), }], xIsNext: true, }; }
render() {
return (
<div className="game">
<div className="game-board">
<Board />
</div>
<div className="game-info">
<div>{/* status */}</div>
<ol>{/* TODO */}</ol>
</div>
</div>
);
}
}
Després hem de fer que el component Board rebi els props squares
i onClick
del component Game. Com que ara tenim un sol gestor de clic al Board per a molts Squares, necessitem passar la ubicació de cada Square al gestor onClick
per indicar quin cuadrat es clicat. Aquí hi ha els passos requerits per transformar el component Board:
- Eliminar el
constructor
al Board. - Substituir
this.state.squares[i]
perthis.props.squares[i]
al mètoderenderSquare
del component Board. - Substituir
this.handleClick(i)
perthis.props.onClick(i)
al mètoderenderSquare
del component Board.
El component Board ara es veu així:
class Board extends React.Component {
handleClick(i) {
const squares = this.state.squares.slice();
if (calculateWinner(squares) || squares[i]) {
return;
}
squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({
squares: squares,
xIsNext: !this.state.xIsNext,
});
}
renderSquare(i) {
return (
<Square
value={this.props.squares[i]} onClick={() => this.props.onClick(i)} />
);
}
render() {
const winner = calculateWinner(this.state.squares);
let status;
if (winner) {
status = 'Winner: ' + winner;
} else {
status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
}
return (
<div>
<div className="status">{status}</div>
<div className="board-row">
{this.renderSquare(0)}
{this.renderSquare(1)}
{this.renderSquare(2)}
</div>
<div className="board-row">
{this.renderSquare(3)}
{this.renderSquare(4)}
{this.renderSquare(5)}
</div>
<div className="board-row">
{this.renderSquare(6)}
{this.renderSquare(7)}
{this.renderSquare(8)}
</div>
</div>
);
}
}
Actualitzarem el mètode render
del component Game per utilitzar l’entrada més recent de l’historial per determinar i mostrar l’estat del joc:
render() {
const history = this.state.history; const current = history[history.length - 1]; const winner = calculateWinner(current.squares); let status; if (winner) { status = 'Winner: ' + winner; } else { status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O'); }
return (
<div className="game">
<div className="game-board">
<Board squares={current.squares} onClick={(i) => this.handleClick(i)} /> </div>
<div className="game-info">
<div>{status}</div> <ol>{/* TODO */}</ol>
</div>
</div>
);
}
Atès que el component ara està renderitzant l’estat del joc, podem eliminar el codi corresponent del mètode render
del Board. Després de refactorizar, el mètode render
es veu així:
render() { return ( <div> <div className="board-row"> {this.renderSquare(0)}
{this.renderSquare(1)}
{this.renderSquare(2)}
</div>
<div className="board-row">
{this.renderSquare(3)}
{this.renderSquare(4)}
{this.renderSquare(5)}
</div>
<div className="board-row">
{this.renderSquare(6)}
{this.renderSquare(7)}
{this.renderSquare(8)}
</div>
</div>
);
}
Finalment, ens cal moure el mètode handleClick
del component Board al component Game. També necessitarem modificar handleClick
ja que l’estat del component Game està estructurat diferent. Al mètode handleClick
de Game, concatenarem la nova entrada de l’historial amb history
.
handleClick(i) {
const history = this.state.history; const current = history[history.length - 1]; const squares = current.squares.slice(); if (calculateWinner(squares) || squares[i]) {
return;
}
squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({
history: history.concat([{ squares: squares, }]), xIsNext: !this.state.xIsNext,
});
}
Nota
A diferència del mètode
push()
dels arrays que has d’estar més familiaritzat, el mètodeconcat()
no muta l’array original, per això ho preferim.
En aquest punt, el component Board només necessita els mètodes renderSquare
i render
. L’estat del joc i el mètode handleClick
haurien d’estar al component Game.
Veure el codi complet en aquest punt
Ara es mostren els moviments anteriors
Com que hem guardat l’historial del joc tres en ratlla, ara podem mostrar-lo al jugador com una llista de moviments anteriors.
Abans hem après que els elements de React són objectes de primera classe a JavaScript; així que podem passar-los d’un lloc a l’altre dins de les nostres aplicacions. Per renderitzar múltiples elements a React podem usar una matriu d’elements de React.
A JavaScript, els arrays tenen un mètode map()
que és comunament usat per mapejar dades a altres dades, per exemple:
const numbers = [1, 2, 3];
const doubled = numbers.map(x => x * 2); // [2, 4, 6]
Usant el mètode map
, podem mapejar el nostre historial de moviments a elements de React representant botons a la pantalla, i mostrant una llista de botons per “saltar” a moviments anteriors.
“Mapejarem” sobre l’historial dins el mètode render
del component Game:
render() {
const history = this.state.history;
const current = history[history.length - 1];
const winner = calculateWinner(current.squares);
const moves = history.map((step, move) => { const desc = move ? 'Go to move #' + move : 'Go to game start'; return ( <li> <button onClick={() => this.jumpTo(move)}>{desc}</button> </li> ); });
let status;
if (winner) {
status = 'Winner: ' + winner;
} else {
status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
}
return (
<div className="game">
<div className="game-board">
<Board
squares={current.squares}
onClick={(i) => this.handleClick(i)}
/>
</div>
<div className="game-info">
<div>{status}</div>
<ol>{moves}</ol> </div>
</div>
);
}
Veure el codi complet en aquest punt
Per cada moviment de l’historial del joc de tres en ratlla, hem creat un element de llista <li>
que conté un botó <button>
. El botó té un gestor onClick
que invoca un mètode anomenat this.jumpTo()
. No hem implementat el mètode jumpTo()
encara. Per ara, hem de veure una llista dels moviments que han ocorregut en el joc i un avís a la consola de les eines de desenvolupador que diu:
Warning: Each child in an array or Iterator should have a unique “key” prop. Check the render method of “Game”.
Ara explicarem que significa l’advertència anterior.
Triant una key
Quan renderitzem una llista, React emmagatzema informació sobre cada element de la llista renderitzat. Quan actualitzem una llista, React necessita determinar què ha canviat. Podríem haver afegit, eliminat, reorganitzat, o actualitzat els elements de la llista.
Imagina canviar de
<li>Alexa: 7 tasks left</li>
<li>Ben: 5 tasks left</li>
a
<li>Ben: 9 tasks left</li>
<li>Claudia: 8 tasks left</li>
<li>Alexa: 5 tasks left</li>
A més dels comptadors actualitzats, un humà llegint això probablement diria que es van intercanviar l’ordre d’Alexa i Ben i van inserir Claudia entre ells. No obstant això, React és un programa d’ordinador i no sap el que intentem. Perquè React no pot saber les nostres intencions, ens cal especificar una propietat key per a cada element de la llista per diferenciar cada un dels seus germans. Una opció seria fer servir els strings alexa
, ben
, claudia
. Si anéssim a mostrar dades d’una base de dades, els ids de base de dades d’Alexa, Ben i Claudia podrien ser usats com keys.
<li key={user.id}>{user.name}: {user.taskCount} tasks left</li>
Quan una llista és rerenderitzada, React pren cada key de l’element de la llista i busca l’element de la llista anterior que coincideixi la key. Si la llista actual té un key que no existia abans, React crea un component. Si a la llista actual li falta un key que existia en la llista anterior, React destrueix el component previ. Si dos keys coincideixen, el component corresponent és mogut. Els keys li diuen a React sobre la identitat de cada component la qual cosa permet a React mantenir el seu estat entre rerenderitzats. Si la key d’un component canvia, el component serà destruït i recreat amb un nou estat.
key
és una propietat especial i reservada a React (igual que amb ref
, una característica més avançada). Quan un element és creat, React extreu la propietat key
i l’emmagatzema directament a l’element retornat. Tot i que el key
es pot veure que pertany a les props
, key
no pot ser referenciada usant this.props.key
. React automàticament fa servir key
per decidir quins components actualitzar. Un component no pot esbrinar sobre la seva key
.
Es recomana fortament que facis servir la key apropiada quan construeixis llistes dinàmiques. Si no tens una key apropiada, potser vols considerar reestructurar les teves dades perquè puguis tenir-la.
Si la key no està especificada, React presentarà una advertència i farà servir l’índex de l’array com a índex per defecte. Usant l’índex de l’array com una key és problemàtic quan intentes reordenar els elements d’una llista o inserir/eliminar elements de la llista. Passar explícitament key={i}
silencia l’advertència però té els mateixos problemes que els índexs de l’array i no és recomanat en la majoria dels casos.
Les keys no necessiten ser globalment úniques; només necessiten ser úniques entre components i els seus germans.
Implementant viatge a través del temps
A l’historial del joc de tres en ratlla, cada moviment anterior té un ID únic associat; és el nombre seqüencial del moviment. Els moviments mai són reordenats, eliminats, o inserits al medi, així que és segur usar els índexs del moviment com una key.
Al mètode render
del component Game, podem afegir el key com <li key={move}
l’advertència de React hauria de desaparèixer:
const moves = history.map((step, move) => {
const desc = move ?
'Go to move #' + move :
'Go to game start';
return (
<li key={move}> <button onClick={() => this.jumpTo(move)}>{desc}</button>
</li>
);
});
Veure el codi complet en aquest punt
Fer clic a qualsevol dels botons de la llista llança un error perquè el mètode jumpTo
no està definit. Abans d’implementar jumpTo
, afegirem stepNumber
a l’estat del component Game per indicar quin pas estem veient actualment.
Primer, afegeix stepNumber: 0
a l’estat inicial al constructor de Game:
class Game extends React.Component {
constructor(props) {
super(props);
this.state = {
history: [{
squares: Array(9).fill(null),
}],
stepNumber: 0, xIsNext: true,
};
}
Després, definirem el mètode jumpTo
al component Game per actualitzar el stepNumber
. També establirem xIsNext
a veritable si el nombre que estem canviant a stepNumber
és parell:
handleClick(i) {
// aquest mètode no ha canviat
}
jumpTo(step) { this.setState({ stepNumber: step, xIsNext: (step % 2) === 0, }); }
render() {
// aquest mètode no ha canviat
}
Ara farem uns petits canvis al mètode handleClick
de Game, el qui es dispara quan fas clic sobre un quadrat.
L’estat stepNumber
que hem afegit ara reflecteix el moviment mostrat a l’usuari. Després de fer un nou moviment, necessitem actualitzar stepNumber
afegint stepNumber: history.length
com a part de l’argument de this.setState
. Això assegura que no ens estanquem mostrant el mateix moviment després d’un de nou realitzat.
També reemplaçarem this.state.history
per this.state.history.slice(0, this.state.stepNumber + 1)
. Això assegura que si “tornem a través del temps” i després fem un nou moviment des d’aquest punt, llançarem tota la història “futura” que ara seria incorrecta.
handleClick(i) {
const history = this.state.history.slice(0, this.state.stepNumber + 1); const current = history[history.length - 1];
const squares = current.squares.slice();
if (calculateWinner(squares) || squares[i]) {
return;
}
squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({
history: history.concat([{
squares: squares
}]),
stepNumber: history.length, xIsNext: !this.state.xIsNext,
});
}
Finalment, modificarem el mètode render
del component Game de sempre renderitzar l’últim moviment a renderitzar el moviment seleccionat actualment d’acord a stepNumber
:
render() {
const history = this.state.history;
const current = history[this.state.stepNumber]; const winner = calculateWinner(current.squares);
// la resta no ha canviat
Si cliquem en qualsevol pas de la història del joc, el tauler tres en ratlla s’hauria d’actualitzar immediatament per mostrar el tauler com es veia després que el pas va ocórrer.
Veure el codi complet en aquest punt
En conclusió
Felicitats! Has creat un joc de tres en ratlla que:
- Et permet jugar al tres en ratlla,
- Indica quan un jugador ha guanyat el joc,
- Emmagatzema l’historial del joc com va progressant,
- Permet als jugadors revisar l’historial del joc i veure versions anteriors del tauler de joc.
Bona feina! Esperem que ara sentis que tens una comprensió decent sobre com funciona React.
Revisa el resultat final aquí: Resultat final.
Si tens temps addicional o vols practicar les teves noves habilitats de React, aquí hi ha algunes idees de millores que pots fer al joc de tres en ratlla, les quals estan llistades en ordre de dificultat creixent:
- Mostra la ubicació per a cada moviment en el format (columna, fila) a la llista de l’historial de moviments.
- Converteix en negreta l’element actualment seleccionat a la llista de moviments.
- Reescriu el Board per utilitzar 2 cicles per fer els quadrats en comptes d’escriure’ls a mà.
- Afegeix un botó de switch que et permeti ordenar els moviments en ordre ascendent o descendent.
- Quan algú guanya, ressalta els 3 quadrats que han fet que guanyi.
- Quan ningú guanya, mostra un missatge que digui que el resultat és un empat.
Al llarg d’aquest tutorial, hem abordat conceptes de React incloent elements, components, props, i estat. Per a una explicació més detallada de cada un d’aquests temes, revisa la resta de la documentació. Per aprendre més sobre definir components, revisa la referència de l’API de React.Component
.