ReactJS: Componentes Reutilizáveis para sua Webapp

Criando e configurando o projeto

Configurando Ambiente

# Instalando Node.js e o NPM
sudo apt install nodejs
sudo apt install npm
# Instalar o create-react-app localmente
npm install create-react-app
# Criando o projeto
./node_modules/.bin/create-react-app projeto
# Rodando o projeto
cd projeto
npm start

JSX, Babel e Webpack

Com o React, nós escrevemos uma linguagem escrita sobre JS. Nós utilizamos a linguagem JSX que nos permite usar marcações XML. O Babel, instalado juntamente com o create-react-app, um compilador (ou um transpiler) de código fonte JavaScript. Ele pegará um código escrito com ECMAScript 6, que ainda só é exportada pelo Node.js, e irá suportar a sintaxe inválida do JS.

Vamos conhecer a utilidade de outro plugin do Babel: ES2015 preset. Com ele, podemos fazer diversas conversões para versões antigas do JS. Ele ainda possui um script que ficar verificando se existem falhas no nosso código.

Webpack é utilizado para converter arquivos e pacotes para algo que rode no browser (no caso, código JavaScript).

Definindo a estrutura do HTML

Importando CSS e Colocando HTML

import './css/pure-min.css'; // Importando o Pure
import './css/side-menu.css'; // Importando o tema do Pure: Responsive Layout

Para colocar o respectivo HTML, basta colocar o HTML padrão do tema a partir da div layout, contido no arquivo index.html, no método render() que é retornado pelo componente do App.js.

Comentando no HTML retornado pelo Componente JS

{/* Comentário */}

Abre e Fecha Tags

Em algumas situações devemos fechar tags que não são obrigatoriamente fechadas no HTML. Isso acontece porque estamos tratando o HTML como um XML, onde para o mesmo toda tag aberta deve ser fechada.

Class e className

Dentro do HTML retornado devemos substituir o parâmetro class por className. Isso acontece porque class é uma palavra reservada do JS.

Consumindo API

Manutenção do Estado do Componente

class App extends Component {
constructor() {
    super(); // Herdar construtor de componente
    this.state = { lista: [{nome:'alberto', email:'alberto.souza@caelum.com.br',senha:'123456'}]}; // Variável para definir estado
}
....
// Dentro do render()
<tbody>
  {
    this.state.lista.map(function(autor){
       return (
         <tr>
           <td>{autor.nome}</td>
           <td>{autor.email}</td>
         </tr>
       );
     })
  }
</tbody>

Atualizando o Estado do Componente

Primeiramente, devemos instalar o JQuery como dependência:

npm install jquery --save

E em seguida importar no componente:

import $ from 'jquery';

Podemos usar duas funções de atualização de estado: componentDidMount(), usada quando o componente acabou de ser montado ( logo após o método render() ser invocado pela primeira vez), e o componentWillMount(), que será chamada antes da invocação do render().

constructor() {
    super();    
    this.state = {lista : []};
}

// Realizando requisições assíncronas
componentDidMount(){  
    $.ajax({
        url:"http://localhost:8080/api/autores",
        dataType: 'json',
        success:function(resposta){    
          this.setState({lista:resposta});
        }.bind(this)
      }
    );          
}

.......

 <tbody>
  {
    this.state.lista.map(function(autor){
      return (
      // Em estados atualizados por ajax, devemos colocar uma key para otimizar a atualização dinâmica do DOM
        <tr key={autor.id}>
          <td>{autor.nome}</td>
          <td>{autor.email}</td>
        </tr>
      );
    })
  }
</tbody>

Cadastros: Eventos e Formulários

 constructor() {
    super();    
    this.state = {lista : [],nome:'',email:'',senha:''}; // Lista e dados do formulário
    this.enviaForm = this.enviaForm.bind(this);
    this.setNome = this.setNome.bind(this);
    this.setEmail = this.setEmail.bind(this);
    this.setSenha = this.setSenha.bind(this);
  }

 ....

 // Evento ao submeter formulário
   enviaForm(evento){
    evento.preventDefault();    
    $.ajax({
      url:'http://localhost:8080/api/autores',
      contentType:'application/json',
      dataType:'json',
      type:'post',
      data:        JSON.stringify({nome:this.state.nome,email:this.state.email,senha:this.state.senha}),
      success: function(resposta){
        this.setState({lista:resposta});        
      }.bind(this),
      error: function(resposta){
        console.log("erro");
      }      
    });
  }

// Alterar campo de nome
  setNome(evento){
    this.setState({nome:evento.target.value});
  }

// Alterar campo de email
  setEmail(evento){
    this.setState({email:evento.target.value});
  }  

// Alterar campo de senha
  setSenha(evento){
    this.setState({senha:evento.target.value});
  }

 .......

 <div className="pure-form pure-form-aligned">
    <form className="pure-form pure-form-aligned" onSubmit={this.enviaForm.bind(this)} method="post">
    <label htmlFor="nome">Nome</label>
        <input id="nome" type="text" name="nome" value={this.state.nome} onChange={this.setNome}/>  
 </div>
 <div className="pure-control-group">
     <label htmlFor="email">Email</label>
     <input id="email" type="email" value={this.state.email} onChange={this.setEmail} />           
 </div>
 <div className="pure-control-group">
     <label htmlFor="senha">Senha</label>
     <input id="email" type="password" name="senha" value={this.state.senha} onChange={this.setSenha} />
 <div className="pure-control-group">                                  
     <label></label>
     <button type="submit" className="pure-button pure-button-primary">Gravar</button>                                    
 </div>
 </form>

Ou, de uma forma mais enxuta:

class FormularioAutor extends Component {

  constructor() {
    super();    
    this.state = {nome:'',email:'',senha:''};
    this.enviaForm = this.enviaForm.bind(this);
  }

  enviaForm(evento){
    evento.preventDefault();    
    $.ajax({
      url:'http://localhost:8080/api/autores',
      contentType:'application/json',
      dataType:'json',
      type:'post',
      data: JSON.stringify({nome:this.state.nome,email:this.state.email,senha:this.state.senha}),
      success: function(novaListagem){
        PubSub.publish('atualiza-lista-autores',novaListagem);        
        this.setState({nome:'',email:'',senha:''});
      }.bind(this),
      error: function(resposta){
        if(resposta.status === 400) {
          new TratadorErros().publicaErros(resposta.responseJSON);
        }
      },
      beforeSend: function(){
        PubSub.publish("limpa-erros",{});
      }      
    });
  }

  salvaAlteracao(nomeInput,evento){
    var campoSendoAlterado = {};
    campoSendoAlterado[nomeInput] = evento.target.value;    
    this.setState(campoSendoAlterado);   
  }

    render() {
        return (
            <div className="pure-form pure-form-aligned">
              <form className="pure-form pure-form-aligned" onSubmit={this.enviaForm} method="post">
                <InputCustomizado id="nome" type="text" name="nome" value={this.state.nome} onChange={this.salvaAlteracao.bind(this,'nome')} label="Nome"/>                                              
                <InputCustomizado id="email" type="email" name="email" value={this.state.email} onChange={this.salvaAlteracao.bind(this,'email')} label="Email"/>                                              
                <InputCustomizado id="senha" type="password" name="senha" value={this.state.senha} onChange={this.salvaAlteracao.bind(this,'senha')} label="Senha"/>                                                                      
                <div className="pure-control-group">                                  
                  <label></label>
                  <button type="submit" className="pure-button pure-button-primary">Gravar</button>                                    
                </div>
              </form>             

            </div>  

        );
    }
}

Componentes Reutilizáveis

Criamos primeiro o seguinte arquivo: ./componentes/InputCustomizado

import React, { Component } from 'react';

export default class InputCustomizado extends Component{

	render() {
		return (
			<div className="pure-control-group">
			  <label htmlFor={this.props.id}>{this.props.label}</label>
			  <input id={this.props.id} type={this.props.type} name={this.props.name} value={this.props.value}  onChange={this.props.onChange}/>                  
			</div>			
		);
	}
}

De forma alternativa, podemos utilizar spread operators:

import React, { Component } from 'react';

export default class InputCustomizado extends Component{

	render() {
        return (
            <div className="pure-control-group">
              <label htmlFor={this.props.id}>{this.props.label}</label>
              <input {...this.props}/>  // Spread Operator: mais enxuto                 
              <span className="error">{this.state.msgErro}</span>
            </div>            
        );
    }
}

Importamos esse arquivo dessa forma em nosso App.js:

import InputCustomizado from './componentes/InputCustomizado';

E utilizamos esse componente assim:

<form className="pure-form pure-form-aligned" onSubmit={this.enviaForm} method="post">
     <InputCustomizado id="nome" type="text" name="nome" value={this.state.nome} onChange={this.setNome} label="Nome"/>                                              
     <InputCustomizado id="email" type="email" name="email" value={this.state.email} onChange={this.setEmail} label="Email"/>                                              
     <InputCustomizado id="senha" type="password" name="senha" value={this.state.senha} onChange={this.setSenha} label="Senha"/>                                                                      
     <div className="pure-control-group">                                  
         <label></label>
         <button type="submit" className="pure-button pure-button-primary">Gravar</button>                                    
     </div>
 </form>

Comunicação entre Componentes

Ao desacoplar funcionalidades de uma tela em outros componentes, podemos nos deparar com a situação de termos componentes diferentes que compartilham um mesmo estado. Para solucionar esta situação, temos a disposição 2 soluções: os High-Order Components e a biblioteca pubsub.

Os primeiros, também chamados de wrappers ou boxes, são componentes que possuem um estado onde enquanto uns consomem outros modificam, de maineira que são simples de implementar, mas ainda possui um alto acoplamento. A segunda possui a seguinte ideia: Um componente publica um determinado objeto, enquanto outros se inscrevem para consumir esse objeto, sem saber da existência um do outro.

Componente de Alta Ordem

// Componente Modificador
class FormularioAutor extends Component {

  constructor() {
    super();    
    this.state = {nome:'',email:'',senha:''};
    this.enviaForm = this.enviaForm.bind(this);
    this.setNome = this.setNome.bind(this);
    this.setEmail = this.setEmail.bind(this);
    this.setSenha = this.setSenha.bind(this);
  }

    enviaForm(evento){
    evento.preventDefault();    
    $.ajax({
      url:'http://localhost:8080/api/autores',
      contentType:'application/json',
      dataType:'json',
      type:'post',
      data: JSON.stringify({nome:this.state.nome,email:this.state.email,senha:this.state.senha}),
      success: function(resposta){
        this.props.callbackAtualizaListagem(resposta); // Atualizando a lista
        this.setState({nome:'',email:'',senha:''});
      }.bind(this),
      error: function(resposta){
         // Tratamento de Erros
      }    
    });
  }

  ....
}

// Componente Consumidor
class TabelaAutores extends Component {

    render() {
        return(
        .....
                        <tbody>
                          {
                            this.props.lista.map(function(autor){
                              return (
                                <tr key={autor.id}>
                                  <td>{autor.nome}</td>
                                  <td>{autor.email}</td>
                                </tr>
                              );
                            })
                          }
                        </tbody>
       .......                   
        );
    }
}

// Componente de Alta Ordem
export default class AutorBox extends Component {

  constructor() {
    super();    
    this.state = {lista : []};
    this.atualizaListagem = this.atualizaListagem.bind(this);    
  }

  componentDidMount(){  
    $.ajax({
        url:"http://localhost:8080/api/autores",
        dataType: 'json',
        success:function(resposta){    
          this.setState({lista:resposta});
        }.bind(this)
      }
    );          
  }   

 atualizaListagem(novaLista) {
  this.setState({lista:novaLista});
}

  render(){
    return (
      <div>
        <FormularioAutor callbackAtualizaListagem={this.atualizaListagem}/>
        <TabelaAutores lista={this.state.lista}/>

      </div>
    );
  }
}

PubSub

Instalamos como dependência usando o seguinte comando:

npm install --save pubsub-js

Realizamos o seguinte import em cada módulo que utiliza o PubSub:

import PubSub from 'pubsub-js';

Usamos da seguinte forma:

// Publisher
class FormularioAutor extends Component {

  constructor() {
    super();    
    this.state = {nome:'',email:'',senha:''};
    this.enviaForm = this.enviaForm.bind(this);
    this.setNome = this.setNome.bind(this);
    this.setEmail = this.setEmail.bind(this);
    this.setSenha = this.setSenha.bind(this);
  }

  enviaForm(evento){
    evento.preventDefault();    
    $.ajax({
      url:'http://localhost:8080/api/autores',
      contentType:'application/json',
      dataType:'json',
      type:'post',
      data: JSON.stringify({nome:this.state.nome,email:this.state.email,senha:this.state.senha}),
      success: function(novaListagem){
        PubSub.publish('atualiza-lista-autores',novaListagem); // Publicando nova listagem       
        this.setState({nome:'',email:'',senha:''});
      }.bind(this),
      error: function(resposta){
        // Tratamento de Erros
      }      
    });
    ....
}

....

// Subscriber
export default class AutorBox extends Component {

  constructor() {
    super();    
    this.state = {lista : []};    
  }

  componentDidMount(){  
    $.ajax({
        url:"http://localhost:8080/api/autores",
        dataType: 'json',
        success:function(resposta){    
          this.setState({lista:resposta});
        }.bind(this)
      }
    );          
    // Atualizando a lista
    PubSub.subscribe('atualiza-lista-autores',function(topico,novaLista){
      this.setState({lista:novaLista});
    }.bind(this));
  }  
}

Tratamento de Erros

Em InputCustomizado.js:

export default class InputCustomizado extends Component{

    constructor(){
        super();
        this.state = {msgErro:''};  // Estado da msg de erro
    }

    render() {
        return (
            <div className="pure-control-group">
              <label htmlFor={this.props.id}>{this.props.label}</label>
              <input id={this.props.id} type={this.props.type} name={this.props.name} value={this.props.value}  onChange={this.props.onChange}/>                  
              <span className="error">{this.state.msgErro}</span> // Mostrar Erro
            </div>            
        );
    }

    componentDidMount() {
        // Mostrar o erro apenas para o campo correto
        PubSub.subscribe("erro-validacao",function(topico,erro){            
            if(erro.field === this.props.name){
                this.setState({msgErro:erro.defaultMessage});            
            }
        }.bind(this));

        // Limpar msg ao ser clicado o botão de submissão
        PubSub.subscribe("limpa-erros",function(topico){                        
            this.setState({msgErro:''});                        
        }.bind(this));        
    }
}

Em Autor.js:

class FormularioAutor extends Component {

  constructor() {
    super();    
    this.state = {nome:'',email:'',senha:''};
    this.enviaForm = this.enviaForm.bind(this);
    this.setNome = this.setNome.bind(this);
    this.setEmail = this.setEmail.bind(this);
    this.setSenha = this.setSenha.bind(this);
  }

  enviaForm(evento){
    evento.preventDefault();    
    $.ajax({
      url:'http://localhost:8080/api/autores',
      contentType:'application/json',
      dataType:'json',
      type:'post',
      data: JSON.stringify({nome:this.state.nome,email:this.state.email,senha:this.state.senha}),
      success: function(novaListagem){
        PubSub.publish('atualiza-lista-autores',novaListagem);        
        this.setState({nome:'',email:'',senha:''});
      }.bind(this),
      error: function(resposta){
        if(resposta.status === 400) {
          // Acionar tratador de erros
          new TratadorErros().publicaErros(resposta.responseJSON);
        }
      },
      beforeSend: function(){
        PubSub.publish("limpa-erros",{}); // Sinal de apagar msg de erro
      }      
    });
  }
  .....
}
......

Em TratadorErros.js:

export default class TratadorErros {
    publicaErros(erros){
        for(var i=0;i<erros.errors.length;i++){
            var erro = erros.errors[i];
            PubSub.publish("erro-validacao",erro); // Publicar cada erro ocorrido
        }
    }
}

Suporte a Navegação

Basicamente o react-router possui componentes que nos permitem associar endereços com componentes específicos da aplicação.

Instalação do React Router

npm install react-router-dom --save

Configurando Navegação

Em index.js:

// Imports padrão
import React from 'react';
import ReactDOM from 'react-dom';
// Componentes da app
import App from './App';
import Autor from './Autor';
import Livro from './Livro';
import Home from './Home';
// CSS
import './index.css';
// Importando o React Router
import {BrowserRouter as Router, Route, Switch, Link} from 'react-router-dom';

ReactDOM.render((
        <Router> // Configurar rotas sem reload
            <App> // Módulo central da app e suas rotas
                    <Switch> // Declaração de rotas            
                        <Route exact path="/" component={Home}/> // Rota default
                        <Route path="/autor" component={AutorAdmin}/> // Rota comum
                        <Route path="/livro" component={LivroAdmin}/>                
                    </Switch>            
            </App>
        </Router>

), document.getElementById('root'));

Em App.js:

import AutorBox from './Autor';
import { Link } from 'react-router-dom'; // Disparar rotas

class App extends Component {

  render() {    
    return (
      <div id="layout">

          <a href="#menu" id="menuLink" className="menu-link">

              <span></span>
          </a>

          <div id="menu">
              <div className="pure-menu">
                  <a className="pure-menu-heading" href="#">Company</a>

                  <ul className="pure-menu-list">

                  // Botões que roteiam a página
                      <li className="pure-menu-item"><Link to="/" className="pure-menu-link">Home</Link></li>
                      <li className="pure-menu-item"><Link to="/autor" className="pure-menu-link">Autor</Link></li>
                      <li className="pure-menu-item"><Link to="/livro" className="pure-menu-link">Livro</Link></li>

                  </ul>
              </div>
          </div>

              <div id="main">
                // Local de preenchimento pelos filhos
                // As rotas determinam qual filho preencherá esta lacuna
                {this.props.children}
              </div>            


      </div>     
    );
  }
}

export default App;

Preparando para produção

Para colocar em produção, basta utilizarmos o seguinte comando:

npm run build

Uma pasta, chamadabuild, foi gerada. Você pode usar a infraestrutura que quiser para rodar essa versão do projeto. Podemos usar o pushstate-server, que é escrito em Javascript e roda sobre o Node.js, como um teste de deploy. Basta irmos para a pasta do projeto e usarmos os seguintes comandos:

npm install pushstate-server
./node_modules/.bin/pushstate-server build