Construindo aplicativos reativos com Redux, RxJS e Redux-Observable em React Native
Publicados: 2022-03-11No crescente ecossistema de aplicativos móveis e da Web avançados e avançados, há cada vez mais estados a serem gerenciados, como usuário atual, lista de itens carregados, estado de carregamento, erros e muito mais. Redux é uma solução para este problema mantendo o estado em um objeto global.
Uma das limitações do Redux é que ele não suporta comportamento assíncrono pronto para uso. Uma solução para isso é o redux-observable
, que é baseado no RxJS, uma poderosa biblioteca para programação reativa em JavaScript. RxJS é uma implementação do ReactiveX, uma API para programação reativa originada na Microsoft. O ReactiveX combina alguns dos recursos mais poderosos do paradigma reativo, programação funcional, padrão observador e padrão iterador.
Neste tutorial, aprenderemos sobre o Redux e seu uso com o React. Também exploraremos a programação reativa usando RxJS e como ela pode tornar o trabalho assíncrono tedioso e complexo muito simples.
Por fim, aprenderemos redux-observable, uma biblioteca que aproveita o RxJS para fazer trabalho assíncrono e, em seguida, construiremos um aplicativo em React Native usando Redux e redux-observable.
Restaurado
Como se descreve no GitHub, o Redux é “um contêiner de estado previsível para aplicativos JavaScript”. Ele fornece aos seus aplicativos JavaScript um estado global, mantendo o estado e as ações longe dos componentes do React.
Em uma aplicação React típica sem Redux, temos que passar dados do nó raiz para os filhos por meio de propriedades ou props
. Esse fluxo de dados é gerenciável para aplicativos pequenos, mas pode se tornar muito complexo à medida que seu aplicativo cresce. Redux nos permite ter componentes independentes uns dos outros, assim podemos usá-lo como uma única fonte de verdade.
O Redux pode ser usado no React usando react-redux
, que fornece ligações para componentes do React para ler dados do Redux e despachar ações para atualizar o estado do Redux.
Redux pode ser descrito como três princípios simples:
1. Fonte Única da Verdade
O estado de todo o seu aplicativo é armazenado em um único objeto. Este objeto no Redux é mantido por uma loja. Deve haver uma única loja em qualquer aplicativo Redux.
» console.log(store.getState()) « { user: {...}, todos: {...} }
Para ler dados do Redux em seu componente React, usamos a função connect
do react-redux
. connect
recebe quatro argumentos, todos opcionais. Por enquanto, vamos nos concentrar no primeiro, chamado mapStateToProps
.
/* UserTile.js */ import { connect } from 'react-redux'; class UserTile extends React.Component { render() { return <p>{ this.props.user.name }</p> } } function mapStateToProps(state) { return { user: state.user } } export default connect(mapStateToProps)(UserTile)
No exemplo acima, mapStateToProps
recebe o estado Redux global como seu primeiro argumento e retorna um objeto que será mesclado com as props passadas para <UserTile />
por seu componente pai.
2. O estado é somente leitura
O estado Redux é somente leitura para componentes React, e a única maneira de alterar o estado é emitir uma ação . Uma ação é um objeto simples que representa uma intenção de mudar o estado. Cada objeto de ação deve ter um campo de type
e o valor deve ser uma string. Fora isso, o conteúdo da ação depende totalmente de você, mas a maioria dos aplicativos segue um formato de ação padrão de fluxo, que limita a estrutura de uma ação a apenas quatro chaves:
-
type
Qualquer identificador de string para uma ação. Cada ação deve ter uma ação única. -
payload
Dados opcionais para qualquer ação. Pode ser de qualquer época e contém informações sobre a ação. -
error
Qualquer propriedade booleana opcional definida como true se a ação representar um erro. Isso é análogo a umaPromise. string
identificador dePromise. string
para uma ação. Cada ação deve ter uma ação única. Por convenção, quandoerror
étrue
, apayload
deve ser um objeto de erro. -
meta
Meta pode ser qualquer tipo de valor. Destina-se a qualquer informação extra que não faça parte da carga útil.
Seguem dois exemplos de ações:
store.dispatch({ type: 'GET_USER', payload: '21', }); store.dispatch({ type: 'GET_USER_SUCCESS', payload: { user: { id: '21', name: 'Foo' } } });
3. O estado é alterado com funções puras
O estado global do Redux é alterado usando funções puras chamadas redutores. Um redutor pega o estado e a ação anteriores e retorna o próximo estado. O redutor cria um novo objeto de estado em vez de alterar o existente. Dependendo do tamanho do aplicativo, uma loja Redux pode ter um único redutor ou vários redutores.
/* store.js */ import { combineReducers, createStore } from 'redux' function user(state = {}, action) { switch (action.type) { case 'GET_USER_SUCCESS': return action.payload.user default: return state } } function todos(state = [], action) { switch (action.type) { case 'ADD_TODO_SUCCESS': return [ ...state, { id: uuid(), // a random uuid generator function text: action.text, completed: false } ] case 'COMPLETE_TODO_SUCCESS': return state.map(todo => { if (todo.id === action.id) { return { ...todo, completed: true } } return todo }) default: return state } } const rootReducer = combineReducers({ user, todos }) const store = createStore(rootReducer)
Semelhante à leitura do estado, podemos usar uma função connect
para despachar ações.
/* UserProfile.js */ class Profile extends React.Component { handleSave(user) { this.props.updateUser(user); } } function mapDispatchToProps(dispatch) { return ({ updateUser: (user) => dispatch({ type: 'GET_USER_SUCCESS', user, }), }) } export default connect(mapStateToProps, mapDispatchToProps)(Profile);
RxJS
Programação reativa
A programação reativa é um paradigma de programação declarativa que trata do fluxo de dados em “ streams ” e de sua propagação e alterações. RxJS, uma biblioteca para programação reativa em JavaScript, tem um conceito de observables , que são fluxos de dados que um observador pode assinar , e esse observador recebe dados ao longo do tempo.
Um observador de um observável é um objeto com três funções: next
, error
e complete
. Todas essas funções são opcionais.
observable.subscribe({ next: value => console.log(`Value is ${value}`), error: err => console.log(err), complete: () => console.log(`Completed`), })
A função .subscribe
também pode ter três funções em vez de um objeto.
observable.subscribe( value => console.log(`Value is ${value}`), err => console.log(err), () => console.log(`Completed`) )
Podemos criar um novo observable criando um objeto de um observable
, passando uma função que recebe um assinante também conhecido como observador. O assinante tem três métodos: next
, error
e complete
. O assinante pode chamar next com um valor quantas vezes forem necessárias, e complete
ou error
no final. Depois de chamar complete
ou error
, o observável não enviará nenhum valor pelo fluxo.
import { Observable } from 'rxjs' const observable$ = new Observable(function subscribe(subscriber) { const intervalId = setInterval(() => { subscriber.next('hi'); subscriber.complete() clearInterval(intervalId); }, 1000); }); observable$.subscribe( value => console.log(`Value is ${value}`), err => console.log(err) )
O exemplo acima imprimirá Value is hi
após 1000 milissegundos.
Criar um observável manualmente toda vez pode se tornar verboso e tedioso. Portanto, o RxJS possui muitas funções para criar um observável. Alguns dos mais usados são of
, from
e ajax
.
do
of
pega uma sequência de valores e a converte em um fluxo:
import { of } from 'rxjs' of(1, 2, 3, 'Hello', 'World').subscribe(value => console.log(value)) // 1 2 3 Hello World
a partir de
from
converte quase tudo em um fluxo de valores:

import { from } from 'rxjs' from([1, 2, 3]).subscribe(console.log) // 1 2 3 from(new Promise.resolve('Hello World')).subscribe(console.log) // 'Hello World' from(fibonacciGenerator).subscribe(console.log) // 1 1 2 3 5 8 13 21 ...
ajax
ajax
pega um URL de string ou cria um observável que faz uma solicitação HTTP. ajax
tem uma função ajax.getJSON
, que retorna apenas o objeto de resposta aninhado da chamada AJAX sem quaisquer outras propriedades retornadas por ajax()
:
import { ajax } from 'rxjs/ajax' ajax('https://jsonplaceholder.typicode.com/todos/1').subscribe(console.log) // {request, response: {userId, id, title, completed}, responseType, status} ajax.getJSON('https://jsonplaceholder.typicode.com/todos/1').subscribe(console.log) // {userId, id, title, completed} ajax({ url, method, headers, body }).subscribe(console.log) // {...}
Existem muitas outras maneiras de fazer um observável (você pode ver a lista completa aqui).
Operadores
Os operadores são uma verdadeira potência do RxJS, que possui um operador para quase tudo o que você precisa. Desde o RxJS 6, os operadores não são métodos no objeto observável, mas funções puras aplicadas no observável usando um método .pipe
.
mapa
map
recebe uma única função de argumento e aplica uma projeção em cada elemento no fluxo:
import { of } from 'rxjs' import { map } from 'rxjs/operators' of(1, 2, 3, 4, 5).pipe( map(i=> i * 2) ).subscribe(console.log) // 2, 4, 6, 8, 10
filtro
filter
recebe um único argumento e remove valores do stream que retornam false para a função dada:
import { of } from 'rxjs' import { map, filter } from 'rxjs/operators' of(1, 2, 3, 4, 5).pipe( map(i => i * i), filter(i => i % 2 === 0) ).subscribe(console.log) // 4, 16
flatMap
O operador flatMap
pega uma função que mapeia cada item do steam em outro stream e nivela todos os valores desses streams:
import { of } from 'rxjs' import { ajax } from 'rxjs/ajax' import { flatMap } from 'rxjs/operators' of(1, 2, 3).pipe( flatMap(page => ajax.toJSON(`https://example.com/blog?size=2&page=${page}`)), ).subscribe(console.log) // [ { blog 1 }, { blog 2 }, { blog 3 }, { blog 4 }, { blog 5 }, { blog 6 } ]
mesclar
merge
mescla itens de dois fluxos na ordem em que chegam:
import { interval, merge } from 'rxjs' import { pipe, take, mapTo } from 'rxjs/operators' merge( interval(150).pipe(take(5), mapTo('A')), interval(250).pipe(take(5), mapTo('B')) ).subscribe(console.log) // ABAABAABBB
A lista completa de operadoras está disponível aqui.
Redux-Observável
Por design, todas as ações no Redux são síncronas. Redux-observable é um middleware para Redux que usa fluxos observáveis para realizar trabalho assíncrono e então despacha outra ação no Redux com o resultado desse trabalho assíncrono.
Redux-observable é baseado na ideia de Epics . Um épico é uma função que recebe um fluxo de ações e, opcionalmente, um fluxo de estado e retorna um fluxo de ações.
função (ação$: Observável
Por convenção, toda variável que é um stream (_aka _observable
) termina com um $
. Antes de podermos usar redux-observable, temos que adicioná-lo como um middleware em nossa loja. Como os épicos são fluxos de observáveis e cada ação que sai desse vapor é canalizada de volta para o fluxo, retornar a mesma ação resultará em um loop infinito.
const epic = action$ => action$.pipe( filter(action => action.type === 'FOO'), mapTo({ type: 'BAR' }) // not changing the type of action returned // will also result in an infinite loop ) // or import { ofType } from 'redux-observable' const epic = action$ => action$.pipe( ofType('FOO'), mapTo({ type: BAZ' }) )
Pense nessa arquitetura reativa como um sistema de tubos onde a saída de cada tubo realimenta todos os tubos, incluindo ele mesmo, e também os redutores do Redux. São os filtros em cima desses tubos que decidem o que entra e o que é bloqueado.
Vamos ver como um épico de pingue-pongue funcionaria. Ele pega um ping, o envia para o servidor - e depois que a solicitação é concluída - envia um pong de volta ao aplicativo.
const pingEpic = action$ => action$.pipe( ofType('PING'), flatMap(action => ajax('https://example.com/pinger')), mapTo({ type: 'PONG' }) ) Now, we are going to update our original todo store by adding epics and retrieving users. import { combineReducers, createStore } from 'redux' import { ofType, combineEpics, createEpicMiddleware } from 'redux-observable'; import { map, flatMap } from 'rxjs/operators' import { ajax } from 'rxjs/ajax' // ... /* user and todos reducers defined as above */ const rootReducer = combineReducers({ user, todos }) const epicMiddleware = createEpicMiddleware(); const userEpic = action$ => action$.pipe( ofType('GET_USER'), flatMap(() => ajax.getJSON('https://foo.bar.com/get-user')), map(user => ({ type: 'GET_USER_SUCCESS', payload: user })) ) const addTodoEpic = action$ => action$.pipe( ofType('ADD_TODO'), flatMap(action => ajax({ url: 'https://foo.bar.com/add-todo', method: 'POST', body: { text: action.payload } })), map(data => data.response), map(todo => ({ type: 'ADD_TODO_SUCCESS', payload: todo })) ) const completeTodoEpic = action$ => action$.pipe( ofType('COMPLETE_TODO'), flatMap(action => ajax({ url: 'https://foo.bar.com/complete-todo', method: 'POST', body: { id: action.payload } })), map(data => data.response), map(todo => ({ type: 'COMPLEE_TODO_SUCCESS', payload: todo })) ) const rootEpic = combineEpics(userEpic, addTodoEpic, completeTodoEpic) const store = createStore(rootReducer, applyMiddleware(epicMiddleware)) epicMiddleware.run(rootEpic);
_Importante: Epics são como qualquer outro stream observável no RxJS. Eles podem acabar em um estado completo ou de erro. Após esse estado, o épico e seu aplicativo pararão de funcionar. Então você deve pegar todos os erros potenciais no vapor. Você pode usar o operador __catchError__
para isso. Mais informações: Tratamento de erros em redux-observable .
Um aplicativo Todo reativo
Com alguma interface do usuário adicionada, um aplicativo de demonstração (mínimo) se parece com isso:
Um Tutorial React, Redux e RxJS Resumido
Aprendemos o que são aplicativos reativos. Também aprendemos sobre Redux, RxJS e redux-observable, e até criamos um aplicativo Todo reativo na Expo com React Native. Para desenvolvedores React e React Native, as tendências atuais oferecem algumas opções de gerenciamento de estado muito poderosas.
Mais uma vez, o código-fonte deste aplicativo está no GitHub. Sinta-se à vontade para compartilhar seus pensamentos sobre o gerenciamento de estado para aplicativos reativos nos comentários abaixo.