ReactiveX Redux

Todo mundo que trabalha com o Redux, mais cedo ou mais tarde, terá o problema de ações assíncronas. Mas um aplicativo moderno não pode ser desenvolvido sem eles. São solicitações http para o back-end e todos os tipos de temporizadores / atrasos. Os próprios criadores do Redux falam sem ambiguidade - por padrão, apenas o fluxo de dados síncrono é suportado, todas as ações assíncronas devem ser colocadas no middleware.

Obviamente, isso é muito detalhado e inconveniente, por isso é difícil encontrar um desenvolvedor que use apenas o middleware "nativo". Bibliotecas e estruturas como Thunk, Saga e similares sempre vêm em socorro.

Para a maioria das tarefas, elas são suficientes. Mas e se for necessária uma lógica um pouco mais complexa do que enviar uma solicitação ou fazer um cronômetro? Aqui está um pequeno exemplo:

async dispatch => { setTimeout(() => { try { await Promise .all([fetchOne, fetchTwo]) .then(([respOne, respTwo]) => { dispatch({ type: 'SUCCESS', respOne, respTwo }); }); } catch (error) { dispatch({ type: 'FAILED', error }); } }, 2000); } 

É doloroso até olhar para esse código, mas é simplesmente impossível manter e expandir. O que fazer quando é necessário um tratamento de erro mais sofisticado? E se você precisar de uma solicitação repetida? E se eu quiser reutilizar esse recurso?

Meu nome é Dmitry Samokhvalov, e neste post vou lhe dizer qual é o conceito de Observable e como colocá-lo em prática em conjunto com o Redux, além de comparar tudo isso com os recursos do Redux-Saga.

Por via de regra, em tais casos, tome redux-saga. OK, reescreva as sagas:

 try { yield call(delay, 2000); const [respOne, respTwo] = yield [ call(fetchOne), call(fetchTwo) ]; yield put({ type: 'SUCCESS', respOne, respTwo }); } catch (error) { yield put({ type: 'FAILED', error }); } 

Tornou-se visivelmente melhor - o código é quase linear, parece e lê melhor. Mas expandir e reutilizar ainda é difícil, porque a saga é tão imperativa quanto o thunk.

Existe outra abordagem. Essa é exatamente a abordagem, e não apenas outra biblioteca para escrever código assíncrono. É chamado Rx (eles também são Observáveis, Reativos Reativos, etc.). Vamos usá-lo e reescrever o exemplo em Observável:

 action$ .delay(2000) .switchMap(() => Observable.merge(fetchOne, fetchTwo) .map(([respOne, respTwo]) => ({ type: 'SUCCESS', respOne, respTwo })) .catch(error => ({ type: 'FAILED', error })) 

O código não apenas se tornou simples e diminuiu de volume, mas o próprio princípio de descrever ações assíncronas mudou. Agora não trabalhamos diretamente com consultas, mas realizamos operações em objetos especiais chamados Observable.

É conveniente representar o Observable como uma função que fornece um fluxo (sequência) de valores. O Observable possui três estados principais - próximo ("dê o próximo valor"), erro ("ocorreu um erro") e completo ("os valores terminaram, não há mais nada a dar"). Nesse sentido, é um pouco como Promise, mas difere no fato de que é possível iterar sobre esses valores (e essa é uma das superpotências Observáveis). Você pode agrupar qualquer coisa no Observable - tempos limite, solicitações de HTTP, eventos DOM, apenas objetos js.



A segunda superpotência observável são os operadores. Um operador é uma função que aceita e retorna um Observable, mas executa alguma ação no fluxo de valores. A analogia mais próxima é mapear e filtrar a partir de javascript (a propósito, esses operadores estão em Rx).



Os mais úteis para mim foram os operadores zip, forkJoin e flatMap. Usando o exemplo deles, é mais fácil explicar o trabalho dos operadores.

O operador zip funciona de maneira muito simples - é necessário um pouco de Observable (não mais que 9) e retorna em uma matriz os valores que eles emitem.

 const first = fromEvent("mousedown"); const second = fromEvent("mouseup"); zip(first, second) .subscribe(e => console.log(`${e[0].x} ${e[1].x}`)); //output [119,120] [120,233] … 

Em geral, o trabalho do zip pode ser representado pelo esquema:



O Zip é usado se você possui vários Observáveis ​​e precisa receber valores deles consistentemente (apesar do fato de que eles podem ser emitidos em intervalos diferentes, de forma síncrona ou não). É muito útil ao trabalhar com eventos DOM.

A instrução forkJoin é semelhante ao zip, com uma exceção - ela retorna apenas os valores mais recentes de cada Observável.



Portanto, é razoável usá-lo quando apenas valores finitos do fluxo são necessários.
Um pouco mais complicado é o operador flatMap. Ele pega um Observable como entrada e retorna um novo Observable e mapeia os valores dele para o novo Observable, usando uma função seletora ou outro Observable. Parece confuso, mas o diagrama é bastante simples:



Ainda mais claro no código:

 const observable = of("Hello"); const promise = value => new Promise(resolve => resolve(`${value} World`); observable .flatMap(value => promise(value)) .subscribe(result => console.log(result)); //output "Hello World" 

Na maioria das vezes, o flatMap é usado em solicitações de back-end, juntamente com switchMap e concatMap.
Como posso usar o Rx no Redux? Existe uma maravilhosa biblioteca observável em redux para isso. Sua arquitetura fica assim:



Todos os operadores e ações observáveis ​​são realizados na forma de middleware especial chamado épico. Cada epopeia executa uma ação como entrada, agrupa-a em um Observável e deve retornar a ação, também como um Observável. Você não pode retornar uma ação regular, isso cria um loop infinito. Vamos escrever um pequeno épico que faz um pedido para a API.

 const fetchEpic = action$ => action$ .ofType('FETCH_INFO') .map(() => ({ type: 'FETCH_START' })) .flatMap(() => Observable .from(apiRequest) .map(data => ({ type: 'FETCH_SUCCESS', data })) .catch(error => ({ type: 'FETCH_ERROR', error })) ) 

É impossível fazer isso sem comparar o redux observável e o redux-saga. Parece para muitos que eles estão próximos em termos de funcionalidade e capacidades, mas esse não é o caso. As sagas são uma ferramenta completamente imperativa, essencialmente um conjunto de métodos para trabalhar com efeitos colaterais. Observable é um estilo fundamentalmente diferente de escrever código assíncrono, se você quiser, uma filosofia diferente.

Escrevi vários exemplos para ilustrar as possibilidades e a abordagem para resolver problemas.

Suponha que precisamos implementar um cronômetro que pare por ação. Aqui está o que parece nas sagas:

 while(true) { const timer = yield race({ stopped: take('STOP'), tick: call(wait, 1000) }) if (!timer.stopped) { yield put(actions.tick()) } else { break } } 

Agora use Rx:

 interval(1000) .takeUntil(action$.ofType('STOP')) 


Suponha que exista uma tarefa para implementar uma solicitação com cancelamento em sagas:

 function* fetchSaga() { yield call(fetchUser); } while (yield take('FETCH')) { const fetchSaga = yield fork(fetchSaga); yield take('FETCH_CANCEL'); yield cancel(fetchSaga); } 

Tudo é mais simples no Rx:

 switchMap(() => fetchUser()) .takeUntil(action$.ofType('FETCH_CANCEL')) 

Finalmente, o meu favorito. Implemente uma solicitação de API, em caso de falha, não faça mais que 5 solicitações repetidas com um atraso de 2 segundos. Aqui está o que temos nas sagas:

 for (let i = 0; i < 5; i++) { try { const apiResponse = yield call(apiRequest); return apiResponse; } catch (err) { if(i < 4) { yield delay(2000); } } } throw new Error(); } 

O que acontece no Rx:

 .retryWhen(errors => errors .delay(1000) .take(5)) 

Se você resumir os prós e contras da saga, obtém a seguinte imagem:



As sagas são fáceis de aprender e muito populares, portanto, na comunidade, você pode encontrar receitas para quase todas as ocasiões. Infelizmente, o estilo imperativo impede o uso das sagas de maneira realmente flexível.

Rx tem uma situação completamente diferente:



Pode parecer que Rx é um martelo mágico e uma bala de prata. Infelizmente, isso não é verdade. O limite para entrada de Rx é muito maior, portanto, é mais difícil introduzir uma nova pessoa em um projeto que usa Rx ativamente.

Além disso, ao trabalhar com o Observable, é especialmente importante ter cuidado e sempre entender bem o que está acontecendo. Caso contrário, você poderá encontrar erros não óbvios ou comportamento indefinido.

 action$ .ofType('DELETE') .switchMap(() => Observable .fromPromise(deleteRequest) .map(() => ({ type: 'DELETE_SUCCESS'}))) 

Depois que escrevi uma epopeia que fazia um trabalho bastante simples - com cada ação do tipo 'DELETE', um método API era chamado para remover o item. No entanto, houve problemas durante o teste. O testador reclamou de comportamento estranho - às vezes, quando você clicava no botão excluir, nada acontecia. Verificou-se que o operador switchMap suporta a execução de apenas um Observável por vez, um tipo de proteção contra as condições da corrida.

Como resultado, darei algumas recomendações que sigo e exorto a todos que começarem a trabalhar com o Rx a seguir:

  • Tome cuidado.
  • Verifique a documentação.
  • Verifique na caixa de areia.
  • Escreva testes.
  • Não atire pardais no canhão.

Source: https://habr.com/ru/post/pt460155/


All Articles