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.
- Parte 1: Iniciando com AdonisJS: Autenticação JWT e API REST;
- Parte 2: Criando CRUD e relações em API REST no AdonisJS;
- Parte 3: Upload de imagens e geolocalização no AdonisJS;
- Parte 4: Iniciando com React Native: Navegação e Autenticação com JWT;
- Parte 5: Instalando o Mapbox e listando imóveis no React Native;
- Parte 6: Instalando a Câmera e realizando o cadastro de Imóveis;
- Parte 7: Listando em um Modal os dados detalhados dos Imóveis;
- Parte 8: Iniciando com ReactJS: Navegação e Autenticação com JWT;
- Parte 9: Instalando o Mapbox e listando os imóveis no ReactJS;
- Parte 10: Utilizando o ModalRoute e fazendo upload de imagens;
- Parte 11: Exibindo informações do imóvel com ModalRoute;
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 header
de 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 Container
atravé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!!