v19.2Latest

Jogo da Velha

Você construirá um pequeno jogo da velha durante este tutorial. Este tutorial não assume nenhum conhecimento prévio de React. As técnicas que você aprenderá no tutorial são fundamentais para construir qualquer aplicativo React, e compreendê-lo completamente lhe dará um entendimento profundo do React.

Observação

Este tutorial foi projetado para pessoas que preferemaprender fazendoe querem tentar rapidamente criar algo tangível. Se você prefere aprender cada conceito passo a passo, comece comDescrevendo a UI.

O tutorial está dividido em várias seções:

O que você vai construir?

Neste tutorial, você construirá um jogo da velha interativo com React.

Você pode ver como ficará quando terminar aqui:

Se o código ainda não fizer sentido para você, ou se você não estiver familiarizado com a sintaxe do código, não se preocupe! O objetivo deste tutorial é ajudá-lo a entender o React e sua sintaxe.

Recomendamos que você confira o jogo da velha acima antes de continuar com o tutorial. Uma das características que você notará é que há uma lista numerada à direita do tabuleiro do jogo. Esta lista fornece um histórico de todos os movimentos que ocorreram no jogo e é atualizada conforme o jogo progride.

Depois de brincar com o jogo da velha finalizado, continue rolando. Você começará com um modelo mais simples neste tutorial. Nosso próximo passo é configurá-lo para que você possa começar a construir o jogo.

Configuração para o tutorial

No editor de código ao vivo abaixo, clique emForkno canto superior direito para abrir o editor em uma nova aba usando o site CodeSandbox. O CodeSandbox permite que você escreva código no seu navegador e visualize como seus usuários verão o aplicativo que você criou. A nova aba deve exibir um quadrado vazio e o código inicial para este tutorial.

Observação

Você também pode seguir este tutorial usando seu ambiente de desenvolvimento local. Para fazer isso, você precisa:

  1. Instalar oNode.js
  2. Na aba do CodeSandbox que você abriu anteriormente, pressione o botão no canto superior esquerdo para abrir o menu e, em seguida, escolhaDownload Sandboxnaquele menu para baixar um arquivo dos arquivos localmente
  3. Descompacte o arquivo, depois abra um terminal ecdpara o diretório que você descompactou
  4. Instale as dependências comnpm install
  5. Executenpm startpara iniciar um servidor local e siga as instruções para visualizar o código em execução em um navegador

Se você ficar travado, não deixe que isso o pare! Continue online e tente uma configuração local novamente mais tarde.

Visão geral

Agora que você está configurado, vamos dar uma visão geral do React!

Inspecionando o código inicial

No CodeSandbox você verá três seções principais:

CodeSandbox com código inicial
  1. A seçãoArquivoscom uma lista de arquivos comoApp.js,index.js,styles.cssna pastasrce uma pasta chamadapublic
  2. O editor de códigoonde você verá o código-fonte do seu arquivo selecionado
  3. A seção donavegadoronde você verá como o código que você escreveu será exibido

O arquivoApp.jsdeve estar selecionado na seçãoArquivos. O conteúdo desse arquivo noeditor de códigodeve ser:

A seção donavegadordeve estar exibindo um quadrado com um X dentro, assim:

quadrado preenchido com X

Agora vamos dar uma olhada nos arquivos do código inicial.

App.js

O código emApp.jscria umcomponente. No React, um componente é um pedaço de código reutilizável que representa uma parte de uma interface de usuário. Componentes são usados para renderizar, gerenciar e atualizar os elementos da interface do usuário em sua aplicação. Vamos olhar para o componente linha por linha para ver o que está acontecendo:

A primeira linha define uma função chamadaSquare. A palavra-chave JavaScriptexporttorna esta função acessível fora deste arquivo. A palavra-chavedefaultinforma a outros arquivos que usam seu código que é a função principal em seu arquivo.

A segunda linha retorna um botão. A palavra-chave JavaScriptreturnsignifica que o que vem depois é retornado como um valor para quem chamou a função.<button>é umelemento JSX. Um elemento JSX é uma combinação de código JavaScript e tags HTML que descreve o que você gostaria de exibir.className="square"é uma propriedade do botão oupropque diz ao CSS como estilizar o botão.Xé o texto exibido dentro do botão e</button>fecha o elemento JSX para indicar que qualquer conteúdo seguinte não deve ser colocado dentro do botão.

styles.css

Clique no arquivo chamadostyles.cssna seçãoArquivosdo CodeSandbox. Este arquivo define os estilos para sua aplicação React. Os dois primeirosseletores CSS(* e body) definem o estilo de grandes partes da sua aplicação, enquanto o seletor.squaredefine o estilo de qualquer componente onde a propriedadeclassNameé definida comosquare. No seu código, isso corresponderia ao botão do seu componente Square no arquivoApp.js.

index.js

Clique no arquivo chamadoindex.jsna seçãoArquivosdo CodeSandbox. Você não editará este arquivo durante o tutorial, mas ele é a ponte entre o componente que você criou no arquivoApp.jse o navegador da web.

As linhas 1-5 reúnem todas as peças necessárias:

  • React
  • A biblioteca do React para conversar com navegadores web (React DOM)
  • os estilos para seus componentes
  • o componente que você criou emApp.js.

O restante do arquivo reúne todas as peças e injeta o produto final emindex.htmlna pastapublic.

Construindo o tabuleiro

Vamos voltar paraApp.js. É aqui que você passará o resto do tutorial.

Atualmente o tabuleiro é apenas um único quadrado, mas você precisa de nove! Se você apenas tentar copiar e colar seu quadrado para fazer dois quadrados assim:

Você receberá este erro:

Console

/src/App.js: Elementos JSX adjacentes devem ser agrupados em uma tag envolvente. Você queria um Fragmento JSX<>...</>?

Componentes React precisam retornar um único elemento JSX e não múltiplos elementos JSX adjacentes como dois botões. Para corrigir isso você pode usarFragmentos(<> e </>) para agrupar múltiplos elementos JSX adjacentes assim:

Agora você deve ver:

dois quadrados preenchidos com X

Ótimo! Agora você só precisa copiar e colar algumas vezes para adicionar nove quadrados e…

nove quadrados preenchidos com X em uma linha

Ah não! Os quadrados estão todos em uma única linha, não em uma grade como você precisa para nosso tabuleiro. Para corrigir isso você precisará agrupar seus quadrados em linhas comdivs e adicionar algumas classes CSS. Enquanto faz isso, você dará um número a cada quadrado para ter certeza de saber onde cada quadrado é exibido.

No arquivoApp.js, atualize o componenteSquarepara ficar assim:

O CSS definido emstyles.cssestiliza as divs com aclassName board-row. Agora que você agrupou seus componentes em linhas com asdivs estilizadas, você tem seu tabuleiro de jogo da velha:

tabuleiro de jogo da velha preenchido com números de 1 a 9

Mas agora você tem um problema. Seu componente chamadoSquare, realmente não é mais um quadrado. Vamos corrigir isso mudando o nome paraBoard:

Neste ponto seu código deve se parecer com algo assim:

Nota

Psssst… Isso é muita coisa para digitar! Tudo bem copiar e colar código desta página. No entanto, se você estiver disposto a um pequeno desafio, recomendamos copiar apenas o código que você digitou manualmente pelo menos uma vez.

Passando dados através de props

Em seguida, você vai querer mudar o valor de um quadrado de vazio para “X” quando o usuário clicar no quadrado. Com a forma como você construiu o tabuleiro até agora, você precisaria copiar e colar o código que atualiza o quadrado nove vezes (uma para cada quadrado que você tem)! Em vez de copiar e colar, a arquitetura de componentes do React permite que você crie um componente reutilizável para evitar código duplicado e confuso.

Primeiro, você vai copiar a linha que define seu primeiro quadrado (<button className="square">1</button>) do seu componenteBoardpara um novo componenteSquare:

Em seguida, você atualizará o componenteBoardpara renderizar esse componenteSquare usando a sintaxe JSX:

Observe como, diferentemente dasdivs do navegador, seus próprios componentesBoard e Squaredevem começar com uma letra maiúscula.

Vamos dar uma olhada:

tabuleiro preenchido com uns

Ah, não! Você perdeu os quadrados numerados que tinha antes. Agora cada quadrado diz "1". Para corrigir isso, você usarápropspara passar o valor que cada quadrado deve ter do componente pai (Board) para seu filho (Square).

Atualize o componenteSquarepara ler a propvalueque você passará doBoard:

function Square({ value })indica que o componente Square pode receber uma prop chamadavalue.

Agora você quer exibir essevalueem vez de1dentro de cada quadrado. Tente fazer assim:

Ops, isso não é o que você queria:

tabuleiro preenchido com 'value'

Você queria renderizar a variável JavaScript chamadavaluedo seu componente, não a palavra "value". Para "escapar para o JavaScript" a partir do JSX, você precisa de chaves. Adicione chaves ao redor devalueno JSX assim:

Por enquanto, você deve ver um tabuleiro vazio:

tabuleiro vazio

Isso ocorre porque o componenteBoardainda não passou a propvaluepara cada componenteSquareque ele renderiza. Para corrigir isso, você adicionará a propvaluea cada componenteSquarerenderizado pelo componenteBoard:

Agora você deve ver uma grade de números novamente:

tabuleiro da velha preenchido com números de 1 a 9

Seu código atualizado deve ficar assim:

Criando um componente interativo

Vamos preencher o componenteSquarecom umXquando você clicar nele. Declare uma função chamadahandleClickdentro doSquare. Em seguida, adicioneonClickàs props do elemento JSX do botão retornado peloSquare:

Se você clicar em um quadrado agora, deverá ver um log dizendo"clicked!"na abaConsolena parte inferior da seçãoBrowserno CodeSandbox. Clicar no quadrado mais de uma vez registrará"clicked!"novamente. Logs repetidos no console com a mesma mensagem não criarão mais linhas no console. Em vez disso, você verá um contador incremental ao lado do seu primeiro log"clicked!".

Observação

Se você está seguindo este tutorial usando seu ambiente de desenvolvimento local, precisa abrir o Console do seu navegador. Por exemplo, se você usa o navegador Chrome, pode visualizar o Console com o atalho de tecladoShift + Ctrl + J(no Windows/Linux) ouOption + ⌘ + J(no macOS).

Como próximo passo, você quer que o componente Square "lembre" que foi clicado e o preencha com uma marca "X". Para "lembrar" coisas, os componentes usamestado.

O React fornece uma função especial chamadauseStateque você pode chamar do seu componente para permitir que ele "lembre" coisas. Vamos armazenar o valor atual doSquareno estado e alterá-lo quando oSquarefor clicado.

ImporteuseStateno topo do arquivo. Remova a propvaluedo componenteSquare. Em vez disso, adicione uma nova linha no início doSquareque chamauseState. Faça com que ela retorne uma variável de estado chamadavalue:

valuearmazena o valor esetValueé uma função que pode ser usada para alterar o valor. Onullpassado parauseStateé usado como o valor inicial para esta variável de estado, entãovalueaqui começa igual anull.

Como o componenteSquarenão aceita mais props, você removerá a propvaluede todos os nove componentes Square criados pelo componente Board:

Agora você alterará oSquarepara exibir um "X" quando clicado. Substitua o manipulador de eventosconsole.log("clicked!"); por setValue('X');. Agora seu componenteSquareficará assim:

Ao chamar esta funçãoseta partir de um manipuladoronClick, você está dizendo ao React para renderizar novamente aqueleSquaresempre que seu<button>for clicado. Após a atualização, ovaluedoSquareserá'X', então você verá o "X" no tabuleiro de jogo. Clique em qualquer Square, e "X" deve aparecer:

adicionando Xs ao tabuleiro

Cada Square tem seu próprio estado: ovaluearmazenado em cada Square é completamente independente dos outros. Quando você chama uma funçãosetem um componente, o React atualiza automaticamente os componentes filhos internos também.

Depois de fazer as alterações acima, seu código ficará assim:

React Developer Tools

O React DevTools permite que você verifique as props e o estado dos seus componentes React. Você pode encontrar a aba do React DevTools na parte inferior da seçãonavegadorno CodeSandbox:

React DevTools no CodeSandbox

Para inspecionar um componente específico na tela, use o botão no canto superior esquerdo do React DevTools:

Selecionando componentes na página com o React DevTools
Observação

Para desenvolvimento local, o React DevTools está disponível como uma extensão de navegador paraChrome,Firefox e Edge. Instale-a e a abaComponentsaparecerá nas Ferramentas de Desenvolvedor do seu navegador para sites que usam React.

Completando o jogo

Neste ponto, você tem todos os blocos básicos para o seu jogo da velha. Para ter um jogo completo, agora você precisa alternar a colocação de "X" e "O" no tabuleiro e precisa de uma maneira de determinar um vencedor.

Elevando o estado

Atualmente, cada componenteSquaremantém uma parte do estado do jogo. Para verificar um vencedor em um jogo da velha, oBoardprecisaria de alguma forma saber o estado de cada um dos 9 componentesSquare.

Como você abordaria isso? A princípio, você pode imaginar que oBoardprecisa "perguntar" a cadaSquarepelo estado daqueleSquare. Embora essa abordagem seja tecnicamente possível no React, nós a desencorajamos porque o código se torna difícil de entender, suscetível a bugs e difícil de refatorar. Em vez disso, a melhor abordagem é armazenar o estado do jogo no componente paiBoardem vez de em cadaSquare. O componenteBoardpode dizer a cadaSquareo que exibir passando uma prop, como você fez quando passou um número para cada Square.

Para coletar dados de vários filhos ou para que dois componentes filhos se comuniquem entre si, declare o estado compartilhado em seu componente pai. O componente pai pode passar esse estado de volta para os filhos via props. Isso mantém os componentes filhos sincronizados entre si e com seu pai.

Elevar o estado para um componente pai é comum quando os componentes React são refatorados.

Vamos aproveitar esta oportunidade para experimentar. Edite o componenteBoardpara que ele declare uma variável de estado chamadasquaresque por padrão é um array de 9 nulls correspondendo aos 9 quadrados:

Array(9).fill(null)cria um array com nove elementos e define cada um deles comonull. A chamadauseState()ao redor dela declara uma variável de estadosquaresque inicialmente é definida como esse array. Cada entrada no array corresponde ao valor de um quadrado. Quando você preencher o tabuleiro mais tarde, o arraysquaresficará assim:

Agora seu componenteBoardprecisa passar a propvaluepara cadaSquareque ele renderiza:

Em seguida, você editará o componenteSquarepara receber a propvaluedo componente Board. Isso exigirá remover o rastreamento com estado próprio do componente Square paravaluee a proponClickdo botão:

Neste ponto, você deve ver um tabuleiro de jogo da velha vazio:

tabuleiro vazio

E seu código deve ficar assim:

Cada Square agora receberá uma propvalueque será'X','O'ounullpara quadrados vazios.

Em seguida, você precisa alterar o que acontece quando umSquareé clicado. O componenteBoardagora mantém quais quadrados estão preenchidos. Você precisará criar uma forma para oSquareatualizar o estado doBoard. Como o estado é privado para o componente que o define, você não pode atualizar o estado doBoarddiretamente a partir doSquare.

Em vez disso, você passará uma função do componenteBoardpara o componenteSquare, e você fará com que oSquarechame essa função quando um quadrado for clicado. Você começará com a função que o componenteSquarechamará quando for clicado. Você chamará essa função deonSquareClick:

Em seguida, você adicionará a funçãoonSquareClickàs props do componenteSquare:

Agora você conectará a proponSquareClicka uma função no componenteBoardque você chamará dehandleClick. Para conectaronSquareClick a handleClick, você passará uma função para a proponSquareClickdo primeiro componenteSquare:

Por fim, você definirá a funçãohandleClickdentro do componente Board para atualizar o arraysquaresque mantém o estado do seu tabuleiro:

A funçãohandleClickcria uma cópia do arraysquares (nextSquares) com o método de array JavaScriptslice(). Em seguida,handleClickatualiza o arraynextSquarespara adicionarXao primeiro quadrado (índice[0]).

Chamar a funçãosetSquarespermite que o React saiba que o estado do componente mudou. Isso acionará uma nova renderização dos componentes que usam o estadosquares (Board), bem como seus componentes filhos (os componentesSquareque compõem o tabuleiro).

Observação

O JavaScript suportaclosures, o que significa que uma função interna (por exemplo,handleClick) tem acesso a variáveis e funções definidas em uma função externa (por exemplo,Board). A funçãohandleClickpode ler o estadosquarese chamar o métodosetSquaresporque ambos estão definidos dentro da funçãoBoard.

Agora você pode adicionar X's ao tabuleiro... mas apenas ao quadrado superior esquerdo. Sua funçãohandleClickestá codificada para atualizar o índice do quadrado superior esquerdo (0). Vamos atualizarhandleClickpara poder atualizar qualquer quadrado. Adicione um argumentoià funçãohandleClickque recebe o índice do quadrado a ser atualizado:

Em seguida, você precisa passar esseiparahandleClick. Você poderia tentar definir a proponSquareClickdo quadrado comohandleClick(0)diretamente no JSX assim, mas isso não funcionará:

Eis por que isso não funciona. A chamadahandleClick(0)fará parte da renderização do componente do tabuleiro. ComohandleClick(0)altera o estado do componente do tabuleiro ao chamarsetSquares, todo o seu componente do tabuleiro será renderizado novamente. Mas isso executahandleClick(0)novamente, levando a um loop infinito:

Console

Muitas re-renderizações. O React limita o número de renderizações para evitar um loop infinito.

Por que esse problema não aconteceu antes?

Quando você estava passandoonSquareClick={handleClick}, você estava passando a funçãohandleClickcomo uma prop. Você não estava chamando-a! Mas agora você estáchamandoessa função imediatamente—note os parênteses emhandleClick(0)—e é por isso que ela executa cedo demais. Você nãoquerchamarhandleClickaté que o usuário clique!

Você poderia corrigir isso criando uma função comohandleFirstSquareClickque chamahandleClick(0), uma função comohandleSecondSquareClickque chamahandleClick(1), e assim por diante. Você passaria (em vez de chamar) essas funções como props, comoonSquareClick={handleFirstSquareClick}. Isso resolveria o loop infinito.

No entanto, definir nove funções diferentes e dar um nome a cada uma delas é muito verboso. Em vez disso, vamos fazer isso:

Observe a nova sintaxe() =>. Aqui,() => handleClick(0)é umaarrow function,que é uma maneira mais curta de definir funções. Quando o quadrado é clicado, o código após a "seta"=>será executado, chamandohandleClick(0).

Agora você precisa atualizar os outros oito quadrados para chamarhandleClicka partir das arrow functions que você passa. Certifique-se de que o argumento para cada chamada dehandleClickcorresponda ao índice do quadrado correto:

Agora você pode novamente adicionar X's a qualquer quadrado no tabuleiro clicando neles:

preenchendo o tabuleiro com X

Mas desta vez, todo o gerenciamento de estado é tratado pelo componenteBoard!

Este é o aspecto que seu código deve ter:

Agora que o seu gerenciamento de estado está no componenteBoard, o componente paiBoardpassa props para os componentes filhosSquarepara que possam ser exibidos corretamente. Ao clicar em umSquare, o componente filhoSquareagora pede ao componente paiBoardpara atualizar o estado do tabuleiro. Quando o estado doBoardmuda, tanto o componenteBoardquanto cada componente filhoSquaresão re-renderizados automaticamente. Manter o estado de todos os quadrados no componenteBoardpermitirá que ele determine o vencedor no futuro.

Vamos recapitular o que acontece quando um usuário clica no quadrado superior esquerdo do seu tabuleiro para adicionar umXa ele:

  1. Clicar no quadrado superior esquerdo executa a função que obuttonrecebeu como sua proponClickdo componenteSquare. O componenteSquarerecebeu essa função como sua proponSquareClickdo componenteBoard. O componenteBoarddefiniu essa função diretamente no JSX. Ela chamahandleClickcom um argumento de0.
  2. handleClickusa o argumento (0) para atualizar o primeiro elemento do arraysquares de nullparaX.
  3. O estadosquaresdo componenteBoardfoi atualizado, então o componenteBoarde todos os seus filhos são renderizados novamente. Isso faz com que a propvaluedo componenteSquarecom índice0mude denullparaX.

No final, o usuário vê que o quadrado superior esquerdo mudou de vazio para ter umXapós clicar nele.

Observação

O atributo<button>do elemento DOMonClicktem um significado especial para o React porque é um componente integrado. Para componentes personalizados como Square, a nomenclatura fica a seu critério. Você pode dar qualquer nome à proponSquareClickdoSquareou à funçãohandleClickdoBoard, e o código funcionaria da mesma forma. No React, é convencional usar nomesonSomethingpara props que representam eventos ehandleSomethingpara as definições de função que lidam com esses eventos.

Por que a imutabilidade é importante

Observe como emhandleClick, você chama.slice()para criar uma cópia do arraysquaresem vez de modificar o array existente. Para explicar o porquê, precisamos discutir a imutabilidade e por que é importante aprendê-la.

Geralmente, existem duas abordagens para alterar dados. A primeira abordagem émutaros dados alterando diretamente seus valores. A segunda abordagem é substituir os dados por uma nova cópia que tenha as alterações desejadas. Veja como ficaria se você mutasse o arraysquares:

E veja como ficaria se você alterasse os dados sem mutar o arraysquares:

O resultado é o mesmo, mas ao não mutar (alterar os dados subjacentes) diretamente, você ganha vários benefícios.

A imutabilidade torna a implementação de funcionalidades complexas muito mais fácil. Mais adiante neste tutorial, você implementará uma funcionalidade de "viagem no tempo" que permite revisar o histórico do jogo e "voltar" para jogadas passadas. Essa funcionalidade não é específica para jogos — a capacidade de desfazer e refazer certas ações é um requisito comum para aplicativos. Evitar a mutação direta dos dados permite manter versões anteriores dos dados intactas e reutilizá-las posteriormente.

Há também outro benefício da imutabilidade. Por padrão, todos os componentes filhos são renderizados automaticamente quando o estado de um componente pai muda. Isso inclui até mesmo os componentes filhos que não foram afetados pela mudança. Embora a re-renderização em si não seja perceptível para o usuário (você não deve tentar ativamente evitá-la!), você pode querer pular a re-renderização de uma parte da árvore que claramente não foi afetada por motivos de desempenho. A imutabilidade torna muito barato para os componentes compararem se seus dados mudaram ou não. Você pode aprender mais sobre como o React escolhe quando re-renderizar um componente ema referência da API memo.

Alternando os turnos

Agora é hora de corrigir um grande defeito neste jogo da velha: os "O"s não podem ser marcados no tabuleiro.

Você definirá a primeira jogada como "X" por padrão. Vamos acompanhar isso adicionando outro pedaço de estado ao componente Board:

Cada vez que um jogador faz uma jogada,xIsNext(um booleano) será invertido para determinar qual jogador vai a seguir e o estado do jogo será salvo. Você atualizará a funçãohandleClickdoBoardpara inverter o valor dexIsNext:

Agora, ao clicar em diferentes quadrados, eles alternarão entreX e O, como deveriam!

Mas espere, há um problema. Tente clicar no mesmo quadrado várias vezes:

O sobrescrevendo um X

O Xé sobrescrito por umO! Embora isso adicionasse uma reviravolta muito interessante ao jogo, vamos seguir as regras originais por enquanto.

Quando você marca um quadrado com umXou umO, você não está primeiro verificando se o quadrado já tem um valorXouO. Você pode corrigir issoretornando antecipadamente. Você verificará se o quadrado já tem umXou umO. Se o quadrado já estiver preenchido, você fará umreturnna funçãohandleClickantecipadamente—antes de tentar atualizar o estado do tabuleiro.

Agora você só pode adicionarXouOa quadrados vazios! Aqui está como seu código deve ficar neste ponto:

Declarando um vencedor

Agora que os jogadores podem alternar turnos, você vai querer mostrar quando o jogo é vencido e não há mais jogadas a fazer. Para fazer isso, você adicionará uma função auxiliar chamadacalculateWinnerque recebe um array de 9 quadrados, verifica se há um vencedor e retorna'X','O'ounullconforme apropriado. Não se preocupe muito com a funçãocalculateWinner; ela não é específica do React:

Observação

Não importa se você definecalculateWinnerantes ou depois doBoard. Vamos colocá-la no final para que você não precise rolar além dela toda vez que editar seus componentes.

Você chamarácalculateWinner(squares)na funçãohandleClickdo componenteBoardpara verificar se um jogador venceu. Você pode realizar essa verificação ao mesmo tempo em que verifica se um usuário clicou em um quadrado que já tem umXou umO. Gostaríamos de retornar antecipadamente em ambos os casos:

Para informar aos jogadores quando o jogo termina, você pode exibir um texto como "Vencedor: X" ou "Vencedor: O". Para fazer isso, você adicionará uma seçãostatusao componenteBoard. O status exibirá o vencedor se o jogo tiver terminado e, se o jogo estiver em andamento, mostrará de qual jogador é a próxima vez:

Parabéns! Agora você tem um jogo da velha funcional. E você acabou de aprender o básico do React também. Entãovocêé o verdadeiro vencedor aqui. Veja como o código deve ficar:

Adicionando viagem no tempo

Como um exercício final, vamos tornar possível "voltar no tempo" para os movimentos anteriores do jogo.

Armazenando um histórico de movimentos

Se você tivesse mutado o arraysquares, implementar a viagem no tempo seria muito difícil.

No entanto, você usouslice()para criar uma nova cópia do arraysquaresapós cada movimento e o tratou como imutável. Isso permitirá que você armazene cada versão passada do arraysquarese navegue entre os turnos que já aconteceram.

Você armazenará os arrayssquarespassados em outro array chamadohistory, que você armazenará como uma nova variável de estado. O arrayhistoryrepresenta todos os estados do tabuleiro, do primeiro ao último movimento, e tem uma forma como esta:

Elevando o estado novamente

Agora você escreverá um novo componente de nível superior chamadoGamepara exibir uma lista de movimentos passados. É lá que você colocará o estadohistoryque contém todo o histórico do jogo.

Colocar o estadohistoryno componenteGamepermitirá que você remova o estadosquaresde seu componente filhoBoard. Assim como você "elevou o estado" do componenteSquarepara o componenteBoard, agora você o elevará doBoardpara o componente de nível superiorGame. Isso dá ao componenteGamecontrole total sobre os dados doBoarde permite que ele instrua oBoarda renderizar turnos anteriores a partir dohistory.

Primeiro, adicione um componenteGame com export default. Faça-o renderizar o componenteBoarde alguma marcação:

Observe que você está removendo as palavras-chaveexport defaultantes da declaraçãofunction Board() {e adicionando-as antes da declaraçãofunction Game() {. Isso informa ao seu arquivoindex.jspara usar o componenteGamecomo o componente de nível superior, em vez do seu componenteBoard. As divs adicionais retornadas pelo componenteGameestão criando espaço para as informações do jogo que você adicionará ao tabuleiro mais tarde.

Adicione algum estado ao componenteGamepara rastrear qual jogador é o próximo e o histórico de movimentos:

Observe como[Array(9).fill(null)]é um array com um único item, que por si só é um array de 9nulls.

Para renderizar os quadrados para o movimento atual, você vai querer ler o último array de quadrados dohistory. Você não precisa deuseStatepara isso—você já tem informações suficientes para calculá-lo durante a renderização:

Em seguida, crie uma funçãohandlePlaydentro do componenteGameque será chamada pelo componenteBoardpara atualizar o jogo. PassexIsNext,currentSquares e handlePlaycomo props para o componenteBoard:

Vamos fazer o componenteBoardser totalmente controlado pelas props que recebe. Altere o componenteBoardpara receber três props:xIsNext,squarese uma nova funçãoonPlayque oBoardpode chamar com o array de quadrados atualizado quando um jogador faz um movimento. Em seguida, remova as duas primeiras linhas da funçãoBoardque chamamuseState:

Agora substitua as chamadassetSquares e setXIsNext em handleClickno componenteBoardpor uma única chamada à sua nova funçãoonPlaypara que o componenteGamepossa atualizar oBoardquando o usuário clicar em um quadrado:

O componenteBoardé totalmente controlado pelas props passadas a ele pelo componenteGame. Você precisa implementar a funçãohandlePlayno componenteGamepara que o jogo funcione novamente.

O que a funçãohandlePlaydeve fazer quando for chamada? Lembre-se de que o Board costumava chamarsetSquarescom um array atualizado; agora ele passa o arraysquaresatualizado paraonPlay.

A funçãohandlePlayprecisa atualizar o estado doGamepara acionar uma nova renderização, mas você não tem mais uma funçãosetSquaresque possa chamar—agora você está usando a variável de estadohistorypara armazenar essa informação. Você vai querer atualizar ohistoryanexando o arraysquaresatualizado como uma nova entrada no histórico. Você também quer alternarxIsNext, assim como o Board costumava fazer:

Aqui,[...history, nextSquares]cria um novo array que contém todos os itens emhistory, seguidos pornextSquares. (Você pode ler a...historysintaxe de espalhamentocomo “enumerar todos os itens emhistory”.)

Por exemplo, sehistoryfor[[null,null,null], ["X",null,null]] e nextSquaresfor["X",null,"O"], então o novo array[...history, nextSquares] será [[null,null,null], ["X",null,null], ["X",null,"O"]].

Neste ponto, você moveu o estado para viver no componenteGame, e a interface do usuário deve estar funcionando completamente, assim como estava antes da refatoração. Aqui está como o código deve parecer neste ponto:

Mostrando os movimentos passados

Como você está gravando o histórico do jogo da velha, agora pode exibir uma lista de movimentos passados para o jogador.

Elementos React como<button>são objetos JavaScript comuns; você pode passá-los em sua aplicação. Para renderizar vários itens no React, você pode usar um array de elementos React.

Você já tem um array de movimentoshistoryno estado, então agora precisa transformá-lo em um array de elementos React. Em JavaScript, para transformar um array em outro, você pode usar ométodo de array map:

Você usarámappara transformar seuhistoryde movimentos em elementos React representando botões na tela e exibir uma lista de botões para “pular” para movimentos passados. Vamosmapsobre ohistoryno componente Game:

Você pode ver como seu código deve ficar abaixo. Observe que você deve ver um erro no console das ferramentas de desenvolvedor que diz:

Você corrigirá este erro na próxima seção.

Ao iterar pelo arrayhistorydentro da função que você passou paramap, o argumentosquarespassa por cada elemento dehistory, e o argumentomovepassa por cada índice do array:0,1,2, …. (Na maioria dos casos, você precisaria dos elementos reais do array, mas para renderizar uma lista de movimentos, você só precisará dos índices.)

Para cada movimento no histórico do jogo da velha, você cria um item de lista<li>que contém um botão<button>. O botão tem um manipuladoronClickque chama uma função chamadajumpTo(que você ainda não implementou).

Por enquanto, você deve ver uma lista dos movimentos que ocorreram no jogo e um erro no console das ferramentas de desenvolvimento. Vamos discutir o que significa o erro de "key".

Escolhendo uma chave

Quando você renderiza uma lista, o React armazena algumas informações sobre cada item da lista renderizado. Quando você atualiza uma lista, o React precisa determinar o que mudou. Você pode ter adicionado, removido, reorganizado ou atualizado os itens da lista.

Imagine a transição de

para

Além das contagens atualizadas, um humano lendo isso provavelmente diria que você trocou a ordem de Alexa e Ben e inseriu Claudia entre Alexa e Ben. No entanto, o React é um programa de computador e não sabe o que você pretendia, então você precisa especificar uma propriedadekeypara cada item da lista para diferenciar cada item de lista de seus irmãos. Se seus dados fossem de um banco de dados, os IDs do banco de dados de Alexa, Ben e Claudia poderiam ser usados como chaves.

Quando uma lista é renderizada novamente, o React pega a chave de cada item da lista e procura nos itens da lista anterior por uma chave correspondente. Se a lista atual tiver uma chave que não existia antes, o React cria um componente. Se a lista atual estiver faltando uma chave que existia na lista anterior, o React destrói o componente anterior. Se duas chaves corresponderem, o componente correspondente é movido.

As chaves informam ao React sobre a identidade de cada componente, o que permite que o React mantenha o estado entre as re-renderizações. Se a chave de um componente mudar, o componente será destruído e recriado com um novo estado.

keyé uma propriedade especial e reservada no React. Quando um elemento é criado, o React extrai a propriedadekeye armazena a chave diretamente no elemento retornado. Emborakeypossa parecer que é passada como props, o React usa automaticamentekeypara decidir quais componentes atualizar. Não há como um componente perguntar qualkeyseu pai especificou.

É altamente recomendável que você atribua chaves apropriadas sempre que construir listas dinâmicas.Se você não tiver uma chave apropriada, pode considerar reestruturar seus dados para que tenha.

Se nenhuma chave for especificada, o React reportará um erro e usará o índice do array como chave por padrão. Usar o índice do array como chave é problemático ao tentar reordenar os itens de uma lista ou inserir/remover itens da lista. Passar explicitamentekey={i}silencia o erro, mas tem os mesmos problemas que os índices de array e não é recomendado na maioria dos casos.

As chaves não precisam ser globalmente únicas; elas só precisam ser únicas entre componentes e seus irmãos.

Implementando viagem no tempo

No histórico do jogo da velha, cada movimento passado tem um ID único associado a ele: é o número sequencial do movimento. Os movimentos nunca serão reordenados, excluídos ou inseridos no meio, portanto, é seguro usar o índice do movimento como chave.

Na funçãoGame, você pode adicionar a chave como<li key={move}>, e se você recarregar o jogo renderizado, o erro de "key" do React deve desaparecer:

Antes de implementarjumpTo, você precisa que o componenteGamemantenha o controle de qual etapa o usuário está visualizando no momento. Para fazer isso, defina uma nova variável de estado chamadacurrentMove, com padrão0:

Em seguida, atualize a funçãojumpTodentro deGamepara atualizar essecurrentMove. Você também definiráxIsNextcomotruese o número para o qual você está alterandocurrentMovefor par.

Agora você fará duas alterações na funçãohandlePlaydoGame, que é chamada quando você clica em um quadrado.

  • Se você "voltar no tempo" e então fizer um novo movimento a partir desse ponto, você só quer manter o histórico até aquele ponto. Em vez de adicionarnextSquaresapós todos os itens (sintaxe de espalhamento...) em history, você o adicionará após todos os itens emhistory.slice(0, currentMove + 1)para manter apenas essa parte do histórico antigo.
  • Cada vez que um movimento é feito, você precisa atualizarcurrentMovepara apontar para a entrada mais recente do histórico.

Finalmente, você modificará o componenteGamepara renderizar o movimento atualmente selecionado, em vez de sempre renderizar o movimento final:

Se você clicar em qualquer etapa do histórico do jogo, o tabuleiro do jogo da velha deve atualizar imediatamente para mostrar como o tabuleiro estava após aquela etapa ocorrer.

Limpeza final

Se você observar o código bem de perto, pode notar quexIsNext === truequandocurrentMoveé par exIsNext === falsequandocurrentMoveé ímpar. Em outras palavras, se você souber o valor decurrentMove, então você sempre pode determinar o quexIsNextdeve ser.

Não há motivo para você armazenar ambos no estado. Na verdade, tente sempre evitar estado redundante. Simplificar o que você armazena no estado reduz bugs e torna seu código mais fácil de entender. AltereGamepara que ele não armazenexIsNextcomo uma variável de estado separada e, em vez disso, determine isso com base nocurrentMove:

Você não precisa mais da declaração de estadoxIsNextnem das chamadas parasetXIsNext. Agora, não há chance dexIsNextficar dessincronizado comcurrentMove, mesmo que você cometa um erro ao codificar os componentes.

Conclusão

Parabéns! Você criou um jogo da velha que:

  • Permite que você jogue tic-tac-toe,
  • Indica quando um jogador venceu o jogo,
  • Armazena o histórico de um jogo conforme ele progride,
  • Permite que os jogadores revisem o histórico de um jogo e vejam versões anteriores do tabuleiro.

Bom trabalho! Esperamos que agora você sinta que tem uma compreensão decente de como o React funciona.

Confira o resultado final aqui:

Se você tiver tempo extra ou quiser praticar suas novas habilidades em React, aqui estão algumas ideias de melhorias que você pode fazer no jogo da velha, listadas em ordem crescente de dificuldade:

  1. Para o movimento atual apenas, mostre "Você está no movimento #..." em vez de um botão.
  2. Reescreva oBoardpara usar dois loops para criar os quadrados em vez de codificá-los manualmente.
  3. Adicione um botão de alternância que permite ordenar os movimentos em ordem crescente ou decrescente.
  4. Quando alguém ganhar, destaque os três quadrados que causaram a vitória (e quando ninguém ganhar, exiba uma mensagem sobre o resultado ser um empate).
  5. Exiba a localização de cada movimento no formato (linha, coluna) na lista de histórico de movimentos.

Ao longo deste tutorial, você tocou em conceitos do React, incluindo elementos, componentes, props e estado. Agora que você viu como esses conceitos funcionam ao construir um jogo, confiraPensando em Reactpara ver como os mesmos conceitos do React funcionam ao construir a interface do usuário de um aplicativo.