Java para Web

Aula 12 : Elevação de Estados

Aula 12 - Elevação de Estados

Aborda a técnica de elevação de estados no react e manipulação de eventos através do encapsulamento de entradas

Elevação de Estados

Componentes Fechados no React

As vezes é necessário que a alteração no estado de um componente implique na alteração do estado de outros componentes. O React no entanto não permite que essa alteração seja realizada diretamente.

Por isso, sempre que consideramos o fluxo dos dados do topo para baixo, precisamos elevar o estado dos dados para o componente pai que controle o conjunto de componentes afetados.

Observe os componentes abaixo:

js Blackjack.js
class Blackjack {
    render(){
        return(
            <section>
            <Mesa></Mesa>
            <Jogador></Jogador>
            </section>
        )
    }
}
js Blackjack.js
class Mesa {
    constructor(props){
        super(props)
        this.state = {
            cartas: ["5", "10", "6"]
        }
    }

    render(){
        const html = cartas.map((carta) => {
            return <li>{carta}</li>
        })
        return html
    }
}
js Blackjack.js
class Jogador{
    constructor(props){
        super(props)
        this.state = {
            cartas: ["A", "10"]
        }
    }

    render(){
        const html = cartas.map((carta) => {
            return <li>{carta}</li>
        })
        return html
    }
}

Temos uma classe chamada Blackjack que renderiza dois componentes:Jogador e Mesa

Pelo estado atual dessas três classes, é impossível para o jogador saber quais são as cartas da mesa, e vice versa. Nem mesmo a classe Blackjack pode determinar quem foi o ganhador da partida, já que todos os dados de que precisa para determinar o resultado estão encapsulados dentro do estado de cada uma dos dois componentes filhos.

Dessa forma, a melhor solução é Elevar o estado de ambos componentes filhos, fazendo com que a classe Blackjack tenha acesso aos dados de Jogador eMesa:

js Blackjack.js
class Blackjack {
    constructor(props){
        this.state = {
            "jogador": ["A", "10"],
            "mesa": ["5", "10", "6"]
        }
    }

    render(){
        return(
            <section>
                <Mesa cartas={this.state.mesa}></Mesa>
                <Jogador cartas={this.state.jogador}></Jogador>
            </section>
        )
    }
}

Elevando o estado de ambos os componentes, centralizamos os dados no componente paiBlackjack, que agora é capaz de determinar o vencedor da partida. Dessa forma, o papel dos componentes Jogador e Mesa agora é simplesmente renderizar suas respectivas cartas, que são recebidas através de um atributo na classe pai.

A elevação de estado tem papel fundamental dentro do React porque, com frequência, uma modificação de um dado tem que ser refletida em vários componentes. Dado a natureza dostate privado de cada componente, recomenda-se sempre elevar o statecompartilhado ao elemento pai comum mais próximo.

Outra função importante dessa estratégia é permitir que componentes sejam renderizados novamente apenas se sofrerem alterações. Nesse caso, caso as cartas da mesa mudem, apenas o componenteMesa será modificado.

Encapsulamento de Entradas

Estados dos Inputs no React

Por padrão, elementos de entrada HTML possuem seu próprio estado interno. De forma a manter o React como fonte única dos dados, podemos encapsular as entradas criando os chamados componentes controlados.

Considere o exemplo abaixo:

js
class Formulario{
    constructor(props){
        super(props)
        this.state = {
            "usuario": ""
        }
    }

    render(){
        return(
            <form>
            <input type="email" id="usuario"></input>
            </form>
        )
    }
}

Para garantir que o conteúdo do input usuario será o mesmo encapsulado dentro dostate do componente, vamos encapsular todas as alterações nesse campo usando uma função manipuladora de eventos

js
class Formulario{
    handleUsuario = (event) => {
        this.setState({
            usuario: event.target.value
        })
    }

    render(){
        return(
            <form>
            <input type="email" id="usuario" onChange={this.handleUsuario} value={this.state.usuario}></input>
            </form>
        )
    }
}

As modificações realizadas no componente foram:

  • Uma função chamada handleUsuario que atualiza o estado do componente de acordo com o valor do evento. O valor do evento pode ser acessado através detarget.value
  • O input foi setado com o atributo onChange, chamando a função manipuladora toda vez que o usuário altera o input
  • O input foi setado com o atributo value, garantindo que o valor dele será sempre o valor presente no state

Este encapsulamento pode ser feito com diferentes tipos de entradas, como checkbox etextarea. Entradas do tipo file por exemplo, não podem ser encapsuladas pois não podem ser acessadas programaticamente.

O uso de componentes controlados é incentivado na maioria dos casos. Para exceções, podemos usar componentes não-controlados, onde os dados de formulário são controlados pelo próprio DOM (Documentação)

Múltiplo Encapsulamento no React

Quando temos formulários com grande quantidade de inputs, pode ser tedioso criar uma função manipuladora para cada um deles. Para resolver isso, podemos usar um truque e criar uma função manipuladora que atende todos os input:

class Formulario{

handleInput = (event) => {
    this.setState({
        [event.target.id]: event.target.value
    })
}

render(){
    return(
        <form>
            <input type="usuario" id="usuario" onChange={this.handleInput}></input>
            <input type="email" id="email" onChange={this.handleInput}></input>
            <input type="password" id="senha" onChange={this.handleInput}></input>
        </form>
    )
}
}

Também podemos substituir o event.target.id por event.target.name, caso as entradas possuam essa propriedade definida.

Manipulando Eventos no React

Como já vimos até então, o React possue regras bastante restritas para o acesso a dados entre componentes. O mesmo vale para os métodos da classe. Um componente pai pode passar informações à um componente filho através das props, mas o inverso não é possível. Para que um componente filho envie informações ou realize alterações na classe pai, é necessário injetar métodos através das propriedades.

Considere o exemplo:

js Trono.js
class Trono {
    constructor(props){
        super(props)
        this.state = {
            "dono": null
        }
    }

    render(){
        return (
            <ul>
                <Personagem nome="Daenerys"></Personagem>
                <Personagem nome="John Snow"></Personagem>
                <Personagem nome="Tyrion"></Personagem>
                <Personagem nome="Joffrey"></Personagem>
                <Personagem nome="Robb"></Personagem>
            </ul>
        )
    }
}
js Personagem.js
class Personagem{
    render(){
        return <li>{this.props.render}</li>
    }
}

De forma a permitir que o componente Personagem altere o valor do estadodono da classe Trono, vamos precisar injetar um método nos componentes filhos:

js Trono.js
class Trono {
    constructor(props){
        super(props)
        this.state = {
            "dono": null
        }
    }

    mudarDono(nome){
        this.setState({
            dono: nome
        })
    }

    render(){
        return (
            <ul>
                <Personagem nome="Daenerys" reivindicarTrono={this.mudarDono}></Personagem>
                <Personagem nome="John Snow" reivindicarTrono={this.mudarDono}></Personagem>
                <Personagem nome="Tyrion" reivindicarTrono={this.mudarDono}></Personagem>
                <Personagem nome="Joffrey" reivindicarTrono={this.mudarDono}></Personagem>
                <Personagem nome="Robb" reivindicarTrono={this.mudarDono}></Personagem>
            </ul>
        )
    }
}

Agora que é possível para a classe Personagem acessar o método mudarDono, podemos chamá-lo. Repare que o nome da propriedade para a classe personagem serárevindicarTrono, que será um alias para o método:

js Personagem.js
class Personagem{
    tomarTrono = () => {
        var nome = this.props.nome
        this.props.reivindicarTrono(nome)
    }

    render(){
        return <li>{this.props.render} <button onClick={this.tomarTrono}>Tomar Trono</button></li>
    }
}

Criamos um novo método na classe personagem chamado tomarTrono. Esse método é chamado por um botão, que chama o método no evento onClick. Ou seja, ao clicar no botão, odono do Trono será atualizado com nome do personagem daquele componente

Outros Tópicos

Renderização Condicional

O uso do JSX permite o uso integrado de estruturas de seleção e repetição. Podemos, por exemplo, criar uma função que retorna um elemento diferente dependendo da sua entrada:

js
boasVindas = (logado) => {
    if(logado){
        return <BoasVindasLogado></BoasVindasLogado>
    }
    else{
        return <BoasVindasDeslogado></BoasVindasDeslogado>
    }
}

Os elementos podem ser armazenados em variáveis, dessa forma, podemos usá-los de forma simplificada na renderização:

js
render(){
    var resultado = null

    if (this.props.admin){
        resultado = <AdminPage></AdminPage>
    }

    return resultado
}

Por fim, também podemos usar essa técnica para concatenar elementos:

js
render(){
    var resultado = null

    if (this.props.admin){
        resultado = <AdminPage></AdminPage>
    }

    <mark class="highlight-inline">resultado = <section>{resultado}</section></mark>

    return resultado
}

Lembre-se que o resultado do render só pode ser um único elemento. Este elemento pode ter diversos filhos, porém é obrigatório que a raiz dos elementos seja uma só.

Fragmentos no React

Como já citado, o resultado do render deve ser sempre um único elemento, não importando quantos filhos ele possui. Isso nos obriga a criar um elemento raiz sempre que desejamos retornar mais de um elemento na renderização, por exemplo:

js
render(){
    return(
        <div>
            <MainPage></MainPage>
            <SideBar></SideBar>
        </div>
    )
}

No entanto, essa estratégia pode ser inconveniente, já que pode exigir a criação de elementos desnecessários ou que não possuem boa acomplagem à outros componentes. Em alguns casos, essa estrutura poderia tornar um componente completamente disfuncional. Acompanhe o exemplo abaixo:

js
class Tabela{
    render(){

        var linhaTabela = <LinhaTabela></LinhaTabela>

        return(
            <table>
                <tr>
                    {linhaTabela}
                </tr>
            </table>
        )
    }
}

class LinhaTabela{
    render(){
        return(
            <div>
                <td>1</td>
                <td>CPF</td>
            </div>
        )
    }
}

O uso de um elemento como div na renderização acima é necessário, já que não é possível retornar a dupla de td diretamente no render. No entanto, isso faz com que a tabela não seja renderizada corretamente, já que a sua estrutura deve ficar assim:

js
<table>
    <tr>
        <div>
            <td>1</td>
            <td>CPF</td>
        </div>
    </tr>
</table>

Para resolver este problema, podemos usar o React.Fragment, uma espécie de pseudo-componente que não é renderizado no DOM. Sua função é apenas encapsular elementos:

js
class LinhaTabela{
    render(){
        return(
            <React.Fragment>
                <td>1</td>
                <td>CPF</td>
            </React.Fragment>
        )
    }
}

Outra opção é utilizar simplesmente os elementos <> e </>, que é uma forma reduzida de especificar um fragmento

Listas no React

Para produzir listas de elementos, podemos utilizar função map. Geralmente, criamos uma estrutura para receber e gerar os elementos a partir de um vetor, muito parecido com isto:

js
render(){
    var boys = ["butcher", "hughie", "mother's milk", "frenchie", "kimiko"]
    var lista = boys.map((valor)=>{
        return <Personagem nome={valor}>
    })

    return lista
}

No exemplo acima, os componentes renderizados devem ser:

js
<Personagem nome="butcher"></Personagem>
<Personagem nome="hughie"></Personagem>
<Personagem nome="mother's milk"></Personagem>
<Personagem nome="frenchie"></Personagem>
<Personagem nome="kimiko"></Personagem>

Executar esse código causa um aviso de chave no console. Iremos verificar o uso de chaves em listas a seguir.

Chaves em Listas no React

Ao renderizar uma lista de componentes, o React realiza sua alteração de forma única, como um único elemento. Para que possamos atualizar elementos de uma lista de maneira individual, vamos utilizar um atributo chamado key, que é usado pelo React apenas para identificar itens alterados ou removidos de uma lista:

js
render(){
    
    var items = [
        {_id: "ac9a8b098c090c90", "name": "Goku"},
        {_id: "ac9a8b098c090c91", "name": "Picollo"},
        {_id: "ac9a8b098c090c92", "name": "Frezza"},
        {_id: "ac9a8b098c090c93", "name": "Gohan"},
        {_id: "ac9a8b098c090c94", "name": "Mr. Satan"}
    ]

    var lista = items.map((valor)=>{
        return <Personagem nome={valor.name} key={valor._id}>
    })

    return lista
}

Recomenda-se o uso de um ID estável para cada item de uma lista. Isso garante que o React terá controle total sobre a atualização da lista, realizando novamente a renderização apenas de itens únicos alterados. Em casos onde não existe nenhum tipo de identificador único, uma última alternativa é usar o índice do vetor como key, no entanto, essa alternativa não é recomendada (Explicação aprofundada sobre impactos negativos de usar o índice).

Outro ponto importante é que o atributo key não será inserido no DOM, sendo usado apenas no controle interno do React.

Axios

O Axios é uma biblioteca para realizar requisições HTTP baseada em promises.

Para instalar o axios utilize o comando:

npm install axios

Exemplo de uso:

js Usando o axios
const axios = require('axios');

axios.get('/user?ID=12345')
    .then(function (response) {
        // sucesso
        console.log(response.data);
    })
    .catch(function (error) {
        // erros
        console.log(error);
    })
    .finally(function () {
        // sempre executa ao final da promise
    });

Mais informações na documentação

Mão na Massa

Exercício: Barco Pirata React
Exemplo do exercício

Crie uma interface que mostre os dados de um barco pirata e três localidades diferentes:

  • Pirata, componente que encapsula e renderiza todos os outros
  • BarcoPirata, componente que renderiza o nome, dano tomado e espólio total do barco em três inputs editáveis
  • Libertalia, componente de cidade (dano: 15, espolio: 150)
  • PortoReal, componente de cidade (dano: 50, espolio: 500)
  • Nassau, componente de cidade (dano: 25, espolio: 250)
Todos os componentes de cidade devem apresentar um botão para entrar na cidade. Ao entrar em uma cidade, o barco deve sofrer o dano definido para aquela cidade e somar o espólio definido.

Além disso, o BarcoPirata deve conter um botão para sortear aleatóriamente um nome de navio a partir do endereço abaixo, usando a biblioteca axios:

https://gitlab.com/rVenson/datasets/-/raw/master/fictional/pirates/pirateship_names.json

Não esqueça de encapsular os dados dos input e a manipulação dos eventos

Dica: Não é necessário criar três componentes para cada cidade. Pode-se usar apenas um e passar as informações através das propriedades, ou mesmo utilizar de herança. No entanto, o projeto React recomenda o uso de composição nos componentes ao invés de herança.

Exercícios Complementares

Exercício: Investimentos React

Implemente uma interface usando React onde o usuário poderá entrar com três informações:

  • Investimento
  • Meses
  • Valor total

Caso uma das entradas seja atualizada, todas as outras devem ser atualizadas também seguindo a lógica:

  • O valor total deverá ser igual ao número de meses * investimento
  • O número de meses deverá ser igual ao valor total / investimento
  • O investimento deverá ser igual ao valor total / meses

Exercício: Domain React

Implemente uma interface em React que seja capaz de realizar requisições para a APIdomainsdb.

https://api.domainsdb.info/v1/domains/search?domain=venson.net.br

A interface deve permitir a pesquisa de nomes de dominio e exibir as seguintes informações:

  • Nome de Dominio
  • Data de Atualização
  • País
  • Endereço IP

Exercício: Deck of Cards React

Utilizando a API Deck of Cards, construa uma interface usando React que tenha as seguintes características:

  • Deve permitir ao usuário gerar um novo baralho;
  • Deve permitir ao usuário reiniciar o baralho gerado;
  • Deve permitir retirar uma nova carta do baralho;
  • A cada nova carta retirada, um componente deve renderizar a imagem da carta atual retirada;
  • Um outro componente deve manter uma lista das cartas que foram retiradas