Construindo aplicativos reativos com Redux, RxJS e Redux-Observable em React Native

Publicados: 2022-03-11

No 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.

Restaurado

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:

  1. type Qualquer identificador de string para uma ação. Cada ação deve ter uma ação única.
  2. payload Dados opcionais para qualquer ação. Pode ser de qualquer época e contém informações sobre a ação.
  3. error Qualquer propriedade booleana opcional definida como true se a ação representar um erro. Isso é análogo a uma Promise. string identificador de Promise. string para uma ação. Cada ação deve ter uma ação única. Por convenção, quando error é true , a payload deve ser um objeto de erro.
  4. 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

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 

Mapa

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 

Filtro

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 } ] 

FlatMap

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 

Mesclar

A lista completa de operadoras está disponível aqui.

Redux-Observável

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 , state$: StateObservable ): 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 aplicativo Todo reativo
O código-fonte deste aplicativo está disponível no Github. Experimente o projeto na Expo ou escaneie o código QR acima no aplicativo Expo.

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.