Iniciando com ReactJS: Navegação e Autenticação com JWT

Esse post é a oitava parte da série de posts “Clone AirBnB com AdonisJS, React Native e ReactJS” onde iremos construir do zero uma aplicação web com ReactJS e também uma aplicação mobile com React Native com dados servidos através de uma API REST feita com NodeJS utilizando o framework AdonisJS.

Cara que insano tem sido esses posts do clone do AirBnB. O Diego chegou arrasando com o AdonisJS e o Cláudio não ficou para trás  com o React Native e agora é minha vez de contribuir com você para essa tríade ficar perfeita.

E vamos de ReactJS, então já apertou o sinto? O foguete já vai voar!!!

Obs.: Não se esqueça de rodar a API do Adonis, pois do contrário o app não conseguirá estabelecer conexão.

Iniciando

Primeiro passo é instalar o CLI (Command Line Interface) do ReactJS

npm install -g create-react-app

E criar o projeto:

create-react-app airbnb-web

Feito isso, adicione algumas dependências:

npm install react-router-dom axios styled-components prop-types font-awesome
  • Axios: Cliente HTTP usado para enviar requisições à API;
  • PropTypes: Lib para chegagem de tipo das props de componentes React;
  • ReactRouter:Lib implementação de navegação na aplicação;
  • StyledComponents: Lib usada estilização dos componentes.
  • Font Awesome: Lib de fontes de ícones.

Agora, você precisa definir uma organização na estrutura do projeto:

src/
 |--- assets/   # Aqui ficará as imagens
 |--- configs/  # Variáveis de configurações
 |--- pages/    # As nossas páginas
 |--- services/ # Configuração de serviços utilizados
 |--- styles/   # Estilos globais
 |--- App.js    # Arquivo que conterá configurações principais do App
 |--- index.js  # Ponto de entrada para execução do nosso App
 |--- routes.js # Arquivo contendo as principais rotas do App

Essa será a configuração inicial e a medida que for preciso você vai alterando. Como o create-react-app cria uma estrutura com arquivos e códigos básicos, é preciso retira-los para ficar organizado como mostrei acima. No index.js:

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

ReactDOM.render(<App />, document.getElementById('root'));

E no App.js:

import React from "react";
import Routes from "./routes";

const App = () => <Routes />;
export default App;

Serviços

Nesse sistema existirá dois serviços importantes que serão requisitados em diversas situações, por isso que foi criado o diretório services. Um serviço para a autenticação do usuários, o auth.js, e o outro para consumir os dados da nossa API feita com o AdonisJS, o api.js.

No arquivo services/auth.js, como dito, será tratado a autenticação dos usuários, e para isso há quatro funções login, logout, getToken e isAuthenticated:

export const TOKEN_KEY = "@airbnb-Token";
export const isAuthenticated = () => localStorage.getItem(TOKEN_KEY) !== null;
export const getToken = () => localStorage.getItem(TOKEN_KEY);
export const login = token => {
  localStorage.setItem(TOKEN_KEY, token);
};
export const logout = () => {
  localStorage.removeItem(TOKEN_KEY);
};

Já no services/api.js será definido qual é a API de consumo, para você não ficar passando isso por extenso toda hora, e já pode definir o headerde Authorization passando o token jwt caso o mesmo exista. (Famoso 2 coelhos, uma cajadada rsrs)

import axios from "axios";
import { getToken } from "./auth";

const api = axios.create({
  baseURL: "http://127.0.0.1:3333"
});

api.interceptors.request.use(async config => {
  const token = getToken();
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

export default api;

Aqui foi utilizado o interceptors do Axios, como o nome sugere eleintercepta uma requisição request antes dela efetivamente acontecer, nesse instante é verificado se existe um token no localStorage, e existindo, ele adiciona o Header de Authorization na request. Isso possibilitará o acesso a páginas que precisam de autenticação, assim como o Diego configurou no Insomnia.

Rotas

Como é comum na maioria dos Apps, a navegação entre páginas, será importante neste também. Por ser pequeno, conterá três rotas principaissendo o SignIn, SignUp e App.

  • SignIn: Entrar com as credenciais para acessar o sistema.
  • SignUp: Criar uma nova conta para acessar o sistema.
  • App: Área que contém as properties e permite adicionar novas.
  • NotFound: Para rotas desconhecidas.

Quem vai orquestrar isso será o ReactRouter no arquivo routes.js onde montaremos a seguinte estrutura:

import React from "react";
import { BrowserRouter, Route, Switch, Redirect } from "react-router-dom";

import { isAuthenticated } from "./services/auth";

const PrivateRoute = ({ component: Component, ...rest }) => (
  <Route
    {...rest}
    render={props =>
      isAuthenticated() ? (
        <Component {...props} />
      ) : (
        <Redirect to={{ pathname: "/", state: { from: props.location } }} />
      )
    }
  />
);

const Routes = () => (
  <BrowserRouter>
    <Switch>
      <Route exact path="/" component={() => <h1>Login</h1>} />
      <Route path="/signup" component={() => <h1>SignUp</h1>} />
      <PrivateRoute path="/app" component={() => <h1>App</h1>} />
      <Route path="*" component={() => <h1>Page not found</h1>} />
    </Switch>
  </BrowserRouter>
);

export default Routes;

Se você não entendeu porque crio o PrivateRoute veja esse vídeo do Diego sobre controle de rotas no youtube. Dê o npm start e até o momento você terá isso:

Repara que quando acesso http://localhost:3000/app ele me retorna para a página inicial? Isso é o esperado, uma vez que o usuário ainda não está logado no sistema. E na rota http://localhost:3000/umapaginaqualquer ele exibe o Page Not  Found como o esperado também.

SignUp

Agora que você já tem a estrutura de rotas prontas, precisa definir as páginas do projeto.

Comece com com a página de SignUp. Primeiro defina o formulário para ter noção daquilo que será a página e vá, posteriormente, estilizando o que for necessário com StyledComponent.

Como estou falando de páginas, os arquivos ficarão no diretório pages e para ficar mais organizado você deve criar um diretório para cada página, sendo que o nome dele será o nome da página e conterá pelo menos o arquivo index.js com a lógica do componente e o styles.js com as estilizações.

src/pages/
      |--- SignUp/
              |--- index.js
              |--- styles.js

Primeiro, crie um componente Form e Containeratravés do StyledComponents, mas não estilize ainda. O styled disponibiliza todos os elementos que temos em HTML, e com a sintaxe tagged template string conseguimos passar o CSS estilizando o elemento selecionado.

import styled from "styled-components";

export const Container = styled.div``;

export const Form = styled.form``;

A princípio será usado o elemento sem estilização, para você perceber que é um componente comum.

Em index.js ficará:

import React, { Component } from "react";
import { Link } from "react-router-dom";

import Logo from "../../assets/airbnb-logo.svg";

import { Form, Container } from "./styles";

class SignUp extends Component {
  state = {
    username: "",
    email: "",
    password: "",
    error: ""
  };

  handleSignUp = e => {
    e.preventDefault();
    alert("Eu vou te registrar");
  };

  render() {
    return (
      <Container>
        <Form onSubmit={this.handleSignUp}>
          <img src={Logo} alt="Airbnb logo" />
          {this.state.error && <p>{this.state.error}</p>}
          <input
            type="text"
            placeholder="Nome de usuário"
            onChange={e => this.setState({ username: e.target.value })}
          />
          <input
            type="email"
            placeholder="Endereço de e-mail"
            onChange={e => this.setState({ email: e.target.value })}
          />
          <input
            type="password"
            placeholder="Senha"
            onChange={e => this.setState({ password: e.target.value })}
          />
          <button type="submit">Cadastrar grátis</button>
          <hr />
          <Link to="/">Fazer login</Link>
        </Form>
      </Container>
    );
  }
}

export default SignUp;

O arquivo para o Logo será disponibilizado no repositório, basta copia-lo para o assets.

E para finalizar adicionar a Page no routes.js:

// ...
// Adicionar o import
import SignUp from "./pages/SignUp";
// ...
// Substituir de 
<Route path="/signup" component={() => <h1>SignUp</h1>} />
// Para
<Route path="/signup" component={SignUp} />
// ...

Nesse momento a página estará muito feia, algo parecido com isso:

O ponto que volto a frisar é que estamos utilizando dois elementos do StyledComponent e que na prática é como se você estivesse usando a tag convencional como os demais elementos da página.

Antes que eu me esqueça, você deve definir um estilo global e para isso vamos com o StyledComponent também. Já vamos importar o font-awesome, para utiliza-lo depois. Crie um arquivo global.js no diretório src/styles que havíamos criado anteriormente, com o seguinte conteúdo:

import { injectGlobal } from "styled-components";

import "font-awesome/css/font-awesome.css";

injectGlobal`
* {
  box-sizing: border-box;
  padding: 0;
  margin: 0;
  outline: 0;
}
body, html {
  background: #eee;
  font-family: 'Helvetica Neue', 'Helvetica', Arial, sans-serif;
  text-rendering: optimizeLegibility !important;
  -webkit-font-smoothing: antialiased !important;
  height: 100%;
  width: 100%;
}
`;

E fazer um kuchiyose no jutsu dele, digo importa-lo em App.js:

import React from "react";
import "./styles/global";

// ...

Bora estilizar a página então?? Volte no arquivo styles.js da Page SignUp e faça o seguinte:

import styled from "styled-components";

export const Container = styled.div`
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100vh;
`;

export const Form = styled.form`
  width: 400px;
  background: #fff;
  padding: 20px;
  display: flex;
  flex-direction: column;
  align-items: center;
  img {
    width: 100px;
    margin: 10px 0 40px;
  }
  p {
    color: #ff3333;
    margin-bottom: 15px;
    border: 1px solid #ff3333;
    padding: 10px;
    width: 100%;
    text-align: center;
  }
  input {
    flex: 1;
    height: 46px;
    margin-bottom: 15px;
    padding: 0 20px;
    color: #777;
    font-size: 15px;
    width: 100%;
    border: 1px solid #ddd;
    &::placeholder {
      color: #999;
    }
  }
  button {
    color: #fff;
    font-size: 16px;
    background: #fc6963;
    height: 56px;
    border: 0;
    border-radius: 5px;
    width: 100%;
  }
  hr {
    margin: 20px 0;
    border: none;
    border-bottom: 1px solid #cdcdcd;
    width: 100%;
  }
  a {
    font-size: 16;
    font-weight: bold;
    color: #999;
    text-decoration: none;
  }
`;

E como num passe de mágica temos:

Você percebeu que com o StyledComponent foi estilizado além dos elementos passados, os filhos dele também foram? Isso ocorreu porque os elementos foram encadeados. Funciona como o CSS normal, só que nesse caso você pode colocar os elementos filhos dentro das chaves {}poupando assim ter que escrever por extenso todas as tags do CSS e deixando isso por conta do StyledComponent. É isso mesmo, você pode usar todo o poder do CSS que apenas o elemento e os seus filhos receberão  a estilização!!!! Isso é bruto, bruto, bruto!!!

Criando usuário

Se você observou bem, a estrutura para pegar os dados do formulário está feita passando do input para o state, bem como a função que será chamada quando o botão “Cadastrar Grátis” for pressionado disparando o evento onSubmit.

Só uma ressalva, será necessário habilitar o CORS do servidor AdonisJS que temos, para isso no arquivo config/cors.js procure pela chave origin e ponha true.

O que precisa agora é fazê-la funcionar, importando a api de consumo de dados, adicionando o withRouter para ter acesso as rotas e substituindo a função handleSignUp no arquivos index.js:

// De
import { Link } from "react-router-dom";
// Para
import { Link, withRouter } from "react-router-dom";

// ...
import api from "../../services/api";

// ...
handleSignUp = async e => {
  e.preventDefault();
  const { username, email, password } = this.state;
  if (!username || !email || !password) {
    this.setState({ error: "Preencha todos os dados para se cadastrar" });
  } else {
    try {
      await api.post("/users", { username, email, password });
      this.props.history.push("/");
    } catch (err) {
      console.log(err);
      this.setState({ error: "Ocorreu um erro ao registrar sua conta. T.T" });
    }
  }
};
///...

export default withRouter(SignUp);

Agora a função handleSignUp é assíncrona e por isso foi adicionado o async/await para trabalhar com isso. Os dados do state foram extraídos e feito uma verificação simples adicionando um error ao estado caso falte algum campo preenchido ou continua com o try/catch. Dentro do try foi feito uma requisição com o método POST do HTTP enviando os dados para inserir o usuário novo. Se tudo ocorrer bem você será redirecionado para a rota de login, caso contrário um erro será informado.

Só reforçando que o withRouter é um HOC que adiciona a propriedade history que possibilita mudar de página.

SignIn

Assim como antes, monte uma estrutura de diretórios para página de SignIn:

src/pages/
      |--- SignIn/
              |--- index.js
              |--- styles.js

Dessa vez, serei mais direto com os códigos, então no index.js:

import React, { Component } from "react";
import { Link, withRouter } from "react-router-dom";

import Logo from "../../assets/airbnb-logo.svg";
import api from "../../services/api";
import { login } from "../../services/auth";

import { Form, Container } from "./styles";

class SignIn extends Component {
  state = {
    email: "",
    password: "",
    error: ""
  };

  handleSignIn = async e => {
    e.preventDefault();
    const { email, password } = this.state;
    if (!email || !password) {
      this.setState({ error: "Preencha e-mail e senha para continuar!" });
    } else {
      try {
        const response = await api.post("/sessions", { email, password });
        login(response.data.token);
        this.props.history.push("/app");
      } catch (err) {
        this.setState({
          error:
            "Houve um problema com o login, verifique suas credenciais. T.T"
        });
      }
    }
  };

  render() {
    return (
      <Container>
        <Form onSubmit={this.handleSignIn}>
          <img src={Logo} alt="Airbnb logo" />
          {this.state.error && <p>{this.state.error}</p>}
          <input
            type="email"
            placeholder="Endereço de e-mail"
            onChange={e => this.setState({ email: e.target.value })}
          />
          <input
            type="password"
            placeholder="Senha"
            onChange={e => this.setState({ password: e.target.value })}
          />
          <button type="submit">Entrar</button>
          <hr />
          <Link to="/signup">Criar conta grátis</Link>
        </Form>
      </Container>
    );
  }
}

export default withRouter(SignIn);

A estrutura é semelhante a página de SignUp só que o método disparado no submit é o handleSignIn, que mostra um erro caso falte algum campo ser preenchido e uma estrutura de try/catch para a requisição na API, exibindo um erro caso haja algum problema na requisição. Dessa vez foi necessário guardar a resposta da API, pois ela traz consigo o token que é guardado no localStorage com a função login que criamos em auth.

Para estilizar mais um vez use o StyledComponents, no styles.js:

import styled from "styled-components";

export const Container = styled.div`
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100vh;
`;

export const Form = styled.form`
  width: 400px;
  background: #fff;
  padding: 20px;
  display: flex;
  flex-direction: column;
  align-items: center;
  img {
    width: 100px;
    margin: 10px 0 40px;
  }
  p {
    color: #ff3333;
    margin-bottom: 15px;
    border: 1px solid #ff3333;
    padding: 10px;
    width: 100%;
    text-align: center;
  }
  input {
    flex: 1;
    height: 46px;
    margin-bottom: 15px;
    padding: 0 20px;
    color: #777;
    font-size: 15px;
    width: 100%;
    border: 1px solid #ddd;
    &::placeholder {
      color: #999;
    }
  }
  button {
    color: #fff;
    font-size: 16px;
    background: #fc6963;
    height: 56px;
    border: 0;
    border-radius: 5px;
    width: 100%;
  }
  hr {
    margin: 20px 0;
    border: none;
    border-bottom: 1px solid #cdcdcd;
    width: 100%;
  }
  a {
    font-size: 16;
    font-weight: bold;
    color: #999;
    text-decoration: none;
  }
`;

Para finalizar, chame a  página no routes.js:

// ...
// Adicionar o import
import SignIn from "./pages/SignIn";
// ...
// Substituir de 
<Route exact path="/" component={() => <h1>Login</h1>} />
// Para
<Route exact path="/" component={SignIn} />
// ...

É só isso, não tem mais jeito, acabou, boa sorte…

Caaaalma só quis cantar um pouco HAHAH

Essa primeira parte foi para você criar uma conta e acessar o sistema com ela, e você já deve ter percebido que não tem muito segredo! Ainda vamos adicionar o Mapa, listar propriedades e muito mais! * — *

Esse código está no github, só clicar aqui

Ah, vale lembrar que você pode deixar seu comentário ali embaixo com suas dúvidas, sugestões e até mesmo falando o que está achando da série.

Abração, nos vemos logo!!