Utilizando React+Redux+Firebase

Essencial em React, o Redux vai ajudar muito em seu desenvolvimento de aplicações React + Firebase

Wellington Oliveira Dos Santos
Blog TecSinapse

--

Provavelmente se você não sabe o que é Redux você não necessita dele, ou pelo menos até se deparar com o primeiro problema do fluxo de dados utilizando React. Visando esses problemas que o Facebook criou uma arquitetura unidirecional de dados chamada Flux. E antes de começarmos, necessitamos falar um pouco sobre esse assunto.

Flux e Redux

A arquitetura Flux

A Flux é uma arquitetura de fluxo de dados desenvolvida pelo Facebook como dito acima. O Flux proporciona um modo de gerenciarmos os dados de uma aplicação baseada em componentes em que os termos View, Action, Dispatcher e Store são os únicos que estão no fluxo de dados, sendo este de uma direção só de View para Action, então para o Dispatcher, Store e então atualizando View novamente.

De modo bem simples (mas bem simples mesmo), uma Action (por exemplo remover um usuário de uma lista acessada por vários componentes) disparada em uma View (um componente) é recebida pelo Dispatcher que irá modificar os dados em uma Store (um reducer) e então atualizar outras ou a mesma View que chamou.

E qual o porque da importância dele? Na prática, caso você seja um iniciante você não terá muita noção disso até que sua aplicação esteja como na imagem abaixo:

Se sua aplicação estiver como na esquerda, você se ferrou.

E quando isso acontecer, você terá que mudar muita coisa no seu código.

Enquanto o Flux é a arquitetura, o Redux é sua implementação feita pelo Facebook, podendo utiliza-lo até fora do ambiente React. Ainda existem alternativas para a implementação do Flux como o MobX. Não vamos entrar em muita mais detalhes sobre Redux, você pode ter mais informações aqui e aqui.

De certa forma, as APIs desenvolvidas pelo Firebase para acesso a banco e outras informações não irão necessitar de Redux caso você use elas diretamente. Por quê? Por que se você utilizar o Realtime Database e der um ref.on(()=> {...}) em algum lugar do código, este irá reagir a qualquer alteração dentro do banco do Firebase, logo o bind é direto entre o componente e o Firebase.

Neste artigo vamos direto a prática, baseado no código do artigo anterior, iremos adicionar login e logout em nossa aplicação, podendo utilizar informações do usuário em qualquer lugar do sistema.

Caso você tenha um conhecimento zero em React e Firebase, temos um artigo especial ensinando, também do zero, a criação de uma aplicação nessa arquitetura React + Firebase.

Passo 0: Configurando o Firebase

Inicialmente necessitamos ajustar o método de autenticação do Firebase, para isso dentro de nosso console do Firebase vamos selecionar AUTENTICAÇÃO então MÉTODO DE LOGIN, ativaremos os seguintes provedores de login:

  • Usuário e Senha
  • Google

Habilitado o login, podemos prosseguir para os próximos passos.

Passo 1: Criação de usuários

Vamos adicionar os seguintes packes via npm

npm install redux-persist redux prop-types react-redux recompose --save

Cada um deles será explicado em seu devido tempo, podemos adiantar que são todos muito importantes em qualquer aplicação React.

Para a funcionalidade de login/logout, em nossa camada de services, vamos adicionar os seguintes métodos:

static createUser = (email, password) => {
return firebaseAuth.createUserWithEmailAndPassword(email, password);
};

static login = (email, password) => {
return firebaseAuth.signInWithEmailAndPassword(email, password);
};

static logout = () => {
return firebaseAuth.signOut();
};

static onAuthChange = (callbackLogin, callbackLogout) => {
firebaseAuth.onAuthStateChanged(authUser => {
if (!authUser) {
callbackLogout(authUser);
} else {
callbackLogin(authUser);
}
});
};

Respectivamente os métodos createUser, login, logout e onAuthChange realizam as tarefas de criação de usuário, login, logout e monitoramento do estado da autenticação (se o usuário está logado ou não).

Agora iniciaremos a construção dos componentes, criaremos um formulário para a criação de novos usuários e também para logarmos na aplicação. O arquivo Login.js será criado no diretório ./src/components/Login.

import React, {Component, Fragment} from "react";
import {Button, TextField, Typography} from "material-ui";
import FirebaseService from "../../services/FirebaseService";
import {urls} from "../../utils/urlUtils";
import {withRouter} from "react-router-dom";

class Login extends Component {

state = {
email: '',
password: ''
};

createUser = (event) => {
event.preventDefault();
const {email} = this.state;
const {password} = this.state;

FirebaseService.createUser(email, password).then(
(user) => {
this.props.history.push(urls.home.path);
console.log(user);
}
).catch(
(error) => {
alert(error.message)
}
)
};

handleChange = name => event => {
this.setState({
[name]: event.target.value,
});
};

render = () => {
return (
<Fragment>
<Typography variant="headline" component="h2">Login</Typography>
<form onSubmit={this.login}>
<TextField className="input-field"
type="email"
value={this.state.email}
label="email"
required
onChange={this.handleChange('email')}/>
<TextField className="input-field"
type="password"
value={this.state.password}
label="password"
required
onChange=
{this.handleChange('password')}
/>
<Button onClick={this.createUser}
style={{marginTop: '20px',
display: 'inline-block'}
}>
New User
</Button>

</form>
</Fragment>)
};
}


export default withRouter(Login);

A aplicação necessita de estado por utilizar o Material-ui (é bem mais simples sem ele), o componente é basicamente um formulário, uma vez que criamos este componente, necessitamos modificar dois códigos importantes de nosso sistema:

Em urlUtils.js adicionaremos a nova rota:

export const urls = {
home: { name: 'Home', path:'/'},
data: { name: 'Data', path:'/data'},
add: { name: 'Add', path:'/add'},
login: { name: 'Login', path:'/login'},
};

Em App.js, adicionaremos uma rota para o novo componente:

<Route exact
path={urls.login.path}
render={(props) => <Login {...props}/>}
/>

Passo 2: Login

Uma vez que já podemos criar usuários, adicionaremos a funcionalidade de login. Adicionaremos um método para login dentro do componente de login como abaixo:

login = (event) => {
event.preventDefault();
const {email} = this.state;
const {password} = this.state;
FirebaseService.login(email, password).then(
(user) => {
this.props.history.push(urls.home.path);
console.log(user);
}
).catch(
(error) => {
alert(error.message)
}
)
};

E um botão próximo ao botão de criar novo usuário:

<Button type="submit"
style={{marginTop: '20px', display: 'inline-block'}}>
Login
</Button>

Nosso formulário terá esta cara:

Autenticação de usuários

Ao logar seremos redirecionados a tela de boas-vindas do sistema. Podemos ver que ainda podemos acessar qualquer tela sem a necessidade de autenticação, logo agora utilizaremos o Redux para ter acesso a informação de login em qualquer componente.

Passo 3: Implementando o Redux

Precisamos monitorar o estado da autenticação em algum lugar, pois não é muito aconselhável realizar o logout dentro de uma action. Logo vamos adicionar isto no componente de maior ordem. Em App.js adicionaremos nosso método onAuthChange ao componentDidMount:

componentDidMount() {
FirebaseService.onAuthChange(
(authUser) => this.props.login(authUser),
() => this.props.logout()
);
FirebaseService.getDataList('leituras',
(dataReceived) => this.setState({data: dataReceived})
)
}

Percebe-se que estamos realizando chamadas aos dois métodos

props.login(authUser)
props.logout()

Porém não temos eles em nenhum lugar; eles nem existem mesmo, e nem temos props neste método. Então iremos criar estes métodos, iremos acopla-los em nossas propriedades do componente, criamos uma função mapDispatchToProps para definirmos essas funções.

const mapDispatchToProps = dispatch => {
return {
login: authUser => dispatch(login(authUser)),
logout: () => dispatch(logout()),
}
};

Podemos ver que temos essa função dispatch, e também chamadas para login e logout que ainda não declaramos. Como já dito anteriormente, Flux contém uma via única de dados, o que estamos fazendo aqui é disparar uma ação, esta ação é chamada pelo dispatch e então executada. Agora vamos ver como declara-lá e também como usa-las.

Vamos criar um arquivo chamado actionCreator.js em .src/actions

export const login = (user) => {
return {user, type: 'LOGIN'}
};

export const logout = () => {return {type:'LOGOUT'}};

Estamos definindo dois tipos de ação: login e logout, estas somente definem o tipo de ação e os dados que recebem, mas ainda não sabemos onde essas ações serão capturadas, e neste caso precisamos de um redutor para receber essas informações.

Criaremos um redutor (reducer) em ./src/reducers/rootReducer.js

import FirebaseService from "../services/FirebaseService";
import {combineReducers} from "redux";

export function userReducer(state = null, action) {
if (action.type === 'LOGIN') {
return action.user;
}

if (action.type === 'LOGOUT') {
FirebaseService.logout();
return null;
}

return state;
}

export default combineReducers({userAuth: userReducer});

Este redutor irá receber uma ação, verificar seu tipo, e dado seu tipo irá retornar um valor. Para este reducer funcionar corretamente, e passar informações entre rotas, páginas e mais de uma tela, necessitamos configurar o reducer-persist para salva-las no localStorage. Criaremos um arquivo configureStore.js em ./src/utils com o seguinte conteúdo:

import {createStore} from 'redux';
import {persistReducer, persistStore} from 'redux-persist';
import storage from 'redux-persist/lib/storage';
import rootReducer from '../reducer/rootReducer';

const persistConfig = {
key: 'root',
storage,
};

const persistedReducer = persistReducer(persistConfig, rootReducer);

export default () => {
let store = createStore(persistedReducer);
let persistor = persistStore(store);
return {store, persistor}
}

Após isso enveloparemos nosso componente App dentro de index.js com os providers para o Redux funcionar corretamente.

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './components/App/App';
import registerServiceWorker from './registerServiceWorker';
import {BrowserRouter as Router, Route} from 'react-router-dom';
import {urls} from "./utils/urlUtils";
import {Provider} from 'react-redux';
import configureStore from './utils/configureStore';
import {PersistGate} from 'redux-persist/integration/react'

const {store, persistor} = configureStore();

ReactDOM.render(
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<Router>
<Route path={urls.home.path} component={App}/>
</Router>
</PersistGate>
</Provider>
, document.getElementById('root')
);

registerServiceWorker();

Com isso podemos utilizar a informação do usuário em qualquer parte de nosso projeto.

Passo 4: Logout

E claro, como podemos, vamos utiliza-la em nossa barra superior em TopBar.js

import {urls} from "../../utils/urlUtils";
import {AppBar, IconButton, Toolbar, Typography, withStyles} from "material-ui";
import MenuIcon from "material-ui-icons/Menu";
import ArrowForward from "material-ui-icons/ArrowForward";
import React, {Fragment} from "react";
import {Link, withRouter} from "react-router-dom";
import {compose} from "recompose";
import {connect} from "react-redux";
import {logout} from "../../action/actionCreator";
import PropTypes from 'prop-types';

const styles = {
root: {
flexGrow: 1,
},
flex: {
flex: 1,
},
};

const TopBar = ({userAuth, logout, classes}) => (<AppBar position="static">
<Toolbar>

<IconButton color="inherit" aria-label="Menu" component={props => <Link to={urls.home.path} {...props}/>}>
<MenuIcon/>
</IconButton>

<Typography type="title" color="inherit" className={classes.flex}>
My Awesome React App
</Typography>

{
userAuth &&
<Fragment>
<Typography type="title" color="inherit" style={{marginLeft: '20px'}}>
{userAuth.email}
</Typography>

<IconButton color="inherit" aria-label="Menu" onClick={() => logout()}>
<ArrowForward/>
</IconButton>
</Fragment>
}

</Toolbar>
</AppBar>);

const mapStateToProps = state => {
return {userAuth: state.userAuth}
};

const mapDispatchToProps = dispatch => {
return {
logout: () => dispatch(logout()),
}
};

TopBar.propTypes = {
classes: PropTypes.object.isRequired,
};

export default compose(
withStyles(styles),
withRouter,
connect(mapStateToProps, mapDispatchToProps)
)(TopBar);

Algo novo não mencionado anteriormente é a função withStyles do Material-ui, esta insere classes css baseadas em objetos javascript em componentes React. Além disso, descrevemos os propTypes, que são uma forma clara de tipagem das propriedades de componentes, é extremamente recomendável utilizarmos propTypes em todos nossos componentes.

Adicionando mapStateToProps ao componente, também estamos nos inscrevendo para que qualquer alteração dentro de um reducer reflita em uma propriedade de nosso componente. A cada login/logout o componente renderiza o nome do usuário caso o usuário esteja logado.

Por último adicionamos o um botão com uma chamada para a função de logout. E assim ficou nossa aplicação rodando:

Na barra superior vermelha: e-mail do usuário e botão para logout.

Passo 5: Bloqueando acesso a páginas não autenticadas

Nossa aplicação tem páginas que são visíveis para qualquer usuário independente dele estar autenticado ou não, existem várias formas de bloquear seu acesso; E quando digo várias, são muitas mesmo. Já deixo claro que não vamos implementar a melhor forma deste bloqueio, que seria um Higher-Order Component, porém vamos utilizar uma das mais rápidas, que é criar um Wrapper para verificar a autenticação na aplicação.

Para isso necessitaremos de um componente que verifique se o usuário está logado, e caso não esteja, redirecione para a tela de login. Então criaremos o componente NavigationWrapper.js em ./src/NavigationWrapper/ com o conteúdo:

import {connect} from "react-redux";
import {Redirect, withRouter} from "react-router-dom";
import {compose} from "recompose";
import React from "react";
import {urls} from "../../utils/urlUtils";

const ifNotLoggedGoToLogin = (userAuth, Component, props) => {
return userAuth != null
? <Component {...props}/>
: <Redirect to={urls.login.path}/>
};

const NavigationWrapper = ({userAuth, component, ...otherProps}) => {
return ifNotLoggedGoToLogin(userAuth, component, otherProps);
};

const mapStateToProps = state => {
return {userAuth: state.userAuth}
};

export default compose(withRouter, connect(mapStateToProps))(NavigationWrapper);

Também criaremos o componente NavigationLoggedWrapper.js para que caso o usuário já esteja logado redirecione ele para tela de inicial:

import {connect} from "react-redux";
import {Redirect, withRouter} from "react-router-dom";
import {compose} from "recompose";
import React from "react";
import {urls} from "../../utils/urlUtils";

const ifLoggedGoToHome = (userAuth, Component, props) => {
return userAuth != null
? <Redirect to={urls.home.path}/>
: <Component {...props}/>
};


const NavigationLoggedWrapper = ({userAuth, component, ...otherProps}) => {
return ifLoggedGoToHome(userAuth, component, otherProps);
};

const mapStateToProps = state => {
return {userAuth: state.userAuth}
};

export default compose(withRouter, connect(mapStateToProps))(NavigationLoggedWrapper);

Então, dentro de App.js iremos modificar a declaração de rotas:

<Route exact path={urls.login.path}
render={(props) =>
<NavigationLoggedWrapper component={Login} {...props}/>}
/>
<Route exact path={urls.home.path}
render={(props) =>
<NavigationWrapper component={Welcome} {...props}/>}
/>
<Route exact path={urls.data.path}
render={(props) =>
<NavigationWrapper component={DataTable}
{...props}
data={this.state.data}
/>}
/>
<Route exact path={urls.add.path}
render={(props) =>
<NavigationWrapper component={Add} {...props}/>}
/>
<Route exact path={privateUrls.edit.path}
render={(props) =>
<NavigationWrapper component={Add} {...props}/>}
/>

E então finalizamos este nosso projeto utilizando React + Redux + Firebase. Temos uma aplicação com um CRUD completo com informações em tempo real, login/logout, design agradável e fluído rodando diretamente do hosting do Firebase.

Com essa me despeço, projeto completo está disponível para teste online aqui e seu código aqui. Alguma dúvida? Me procure por alguma rede social para conversarmos de React, Firebase e modos de deixar a vida de dev mais fácil.

Muito obrigado e até o mais um artigo galera. 😄

--

--