Ações em Redux-Saga com Either

Usando estruturas de FP para melhorar seu código!

Image for post
Image for post
Esta foi a primeira imagem que surgiu na busca do Google para “qualquer um” e eu pensei, "É exatamente isso!"

Manipular erros em redux-saga tem sido algo que exploramos de vez em quando, mas nunca encontramos uma solução sólida.

Nós também somos fãs do Flux Standard Action (eu vou me referir a eles como FSA a partir daqui), o que significa que tratamos erros como cidadãos de primeira classe.

O problema

As ações FSA contém dois estados principais, o estado de sucesso, ou “OK”, quando uma ação é completada com sucesso, e seu estado ”Error”, quando aconteceu algum erro durante essa ação.

Como escolhemos usar FSA, isso significa que temos que ̶(̶l̶e̶m̶b̶r̶a̶r̶)̶ verificar se action.error existe, antes de fazer qualquer trabalho em nossos reducers/sagas.

O problema é que muitas vezes nós esquecemos, pois estamos focados em criar features completas e os estados de erro muitas vezes são esquecidos e o código pode ser executado em ações que causam problemas.

Há também a sobrecarga de que todas as sagas que escutem qualquer ação devem verificar action.error e, como nem sempre podemos saber o que vai ser despachado em nossa ação, temos que assumir que poderia vir de qualquer lugar dentro de nossa grande aplicação, inclusive de outras sagas.

A solução

Uma solução é, garantir que cada reducer e saga chequem action.error antes de fazer qualquer trabalho. A maioria de nossas sagas só se interessa por payloads com sucesso, o que leva a muitos:

if (action.error) return;

Precisamos de uma abordagem mais genérica que nos permita:

  • Executar nosso código principal quando conter o estado de sucesso
  • Pegue erros na ação e faça algo útil, se assim desejarmos, quando conter o estado de erro.
  • Tornar explícito qual código é qual e nos forçar a deixar isso claro.

Considerando isso, uma estrutura de dados em particular, que poderia nos ajudar a resolver esse problema, é a estrutura Either.

Quando eu pensei nisso, resolvei olhar na internet para ver se alguém já havia pensado em algo parecido, com certeza tem gente mais inteligente que eu e, de fato, a idéia não é única, porém, ela requer um pouco de estrutura.

Either é um tipo com dois sub-tipos, Left e Right. No nosso cenário, nós não precisamos do tipo principal Either, nós precisamos apenas dos sub-tipos, se você estiver vindo de uma linguagem fortemente tipada, você pode pensar em Either como um tipo de ambos Left e Right, ou até mesmo, como uma interface, se você tem experiência com OOP.

Ambos, Left e Right expõem a mesma interface, que no nosso caso irá incluir map e fold.

Um pouco de explicação sobre Either: O tipo Either se refere a uma interface que será usada para lidar com dois resultados diferentes em uma mesma operação. Se o resultado dessa operação for uma coisa, irá retornar Left e, se for outra coisa, irá retornar Right. É isso, ou aquilo.

No nosso caso, queremos um resultado “Error” ou um resultado ”OK”, usaremos o Left para o nosso estado de erro e Right para o nosso estado de sucesso.

É isso, ou aquilo

Como Left e Right usam a mesma interface, podemos garantir que as funções map e fold, irão existir. Nossa implementação desses dois tipos, farão toda a diferença.

Vamos começar com map. map é uma função que leva uma função e retorna uma nova instância do container em que a função foi chamada.

Considere Array.prototype.map, ele recebe uma função e retorna um array. Nesse caso, o array é o container que a função foi chamada. O map é uma forma de revelar o valor do container, realizando algumas operações nele e depois colocando-o de volta no container, antes de retorná-lo.

Portanto, nossos dois tipos, Left e Right, terão uma função map que recebe uma função e retorna Left ou Right.

Aqui é onde os dois tipos divergem. Enquanto Left e Right terão uma função map, só queremos executar a função passada para o map quando o resultado for Right, ou seja, um resultado OK.

Se estamos lidando com Left, queremos ignorar a função passada para map e, ao invés disso, simplesmente retornar um novo Left.

Vamos escrever uma rápida implementação desses dois tipos:

function Left(value) {
return {
map: () => Left(value)
}
}
function Right(value) {
return {
map: (fn) => Right(fn(value))
}
}

Agora temos o nosso map. Quando for Left, ignoramos a função passada e para Right, executamos a função passando o valor. Em ambos os casos, colocamos o valor resultante em um novo Left ou Right.

Reduxficando

Definimos dois tipos; Left e Right. Eles correspondem aos nossos dois estados; “Error” e ”OK”.

Vamos escrever um exemplo de uso para os nossos tipos. Vamos imaginar que, nossos reducers e sagas receberiam um ou outro tipo (“Error” ou “OK”), com o qual eles podem trabalhar.

Mas antes, vamos ver como era feito essa implementação:

function* productsReceivedSaga({ error, payload }) {
if (error) {
return;
}
/* Realiza algum trabalho */
}

É exatamente isso que queremos evitar!

Agora, utilizando nossa estrutura com Either com Left/Right:

function* productsReceivedSaga({ payload }) {
yield payload.map((p) => {
/* Realiza algum trabalho */
});
}

Aqui já podemos observar melhorias. Nossa ação não precisa se preocupar se ela contém ou não o erro, pois isso já é resolvido pela estrutura Either.

Mas isso ainda não está certo. Em primeiro lugar, nossa função map retornará um tipo Either, mas, para renderizar corretamente os valores para o middleware redux-saga, precisaremos revelar o valor dessa estrutura.

É aí que entra fold. fold é uma função que recebe uma função e a executa com no contexto do seu tipo Either e retorna seu valor pleno, sem envolver ele dentro de um tipo.

function Right(value) {
return {
map: (fn) => Right(fn(value)),
fold: (fn) => fn(value)
}
}

Agora temos uma maneira de revelar nosso valor do Either e fazer algo com ele. Nossa saga pode usar fold e receber o valor de volta usando a função de identidade, x => x.

function* productsReceivedSaga({ payload }) {
yield payload.map((p) => {
/* Realiza algum trabalho */
}).fold(x => x)
}

redux-saga irá receber qualquer valor produzido pelo yield no map.

E os erros? Como tratá-los?

Nós começamos o artigo dizendo que queríamos uma maneira de lidar com erros, para isso, iremos utilizar o tipo Left.

Para ter um controle de erro no nosso código, iremos executar algo somente quando um erro existir, podemos passar um segundo parâmetro para nossa função fold, que será executada ao invés da primeira função.

Chamaremos a função inicial passada fold de _, para indicar que não estamos usando-a no tipo Left.

function Left(value) {
return {
map: () => Left(value),
fold: (_, fn) => fn(value)
}
}
function Right(value) {
return {
map: (fn) => Right(fn(value)),
fold: (fn) => fn(value)
}
}

Podemos atualizar nossa saga:

function* productsReceivedSaga({ payload }) {
yield payload.map((p) => {
/* Realiza algum trabalho */
})
.fold(
x => x,
err => /* aqui você cuida do erro */
)
}

Agora, precisamos de uma maneira de conectar isso com redux, para que nossos reducers/sagas recebam o tipo correto Left ou Right.

Redux Middleware ao resgate

O middleware para isso é bastante simples. Queremos inspecionar nossa ação e, se houver uma propriedade de erro definida, retornamos Left e, se não houver nenhum erro, devolvemos um Right.

Vamos escrever uma função que leva uma ação FSA e nos devolve o tipo correto:

function makeEither(action) {
return action.error
? Left(action.payload)
: Right(action.payload);
}

Voilà! Nós temos tudo o que precisamos para um middleware redux simples que converte as ações em tipos Either:

export default store => next => action => next({
...action,
payload: makeEither(action)
});

Com uma única interface utilizando map e fold, conseguimos eliminar todos aqueles ifs aqui e ali nas nossas ações, deixando o código mais declarativo, removendo boa parte imperativa desse fluxo.

Recapitulando

Nós queríamos uma melhor maneira para nossos reducers/sagas cuidarem do resultado de ações FSA (com ou sem erros), e não queríamos ter que escrever, repetidamente, declarações ifs para verificar cada ação que recebíamos.

Ao usar a estrutura Either, com um simples middleware em redux, asseguramos que tudo dentro de uma função map só será executado quando estivermos em um estado “OK” e fold executará uma das duas funções recebidas, sendo elas do tipo Left ou Right.

Estas não são idéias novas e eu não sou o primeiro a pensar em aplicá-las em redux e redux-saga. Minha esperança é que, ao escrever meus pensamentos aqui, haverá uma referência útil para aqueles que buscam idéias semelhantes.

Obrigado por ler!

Written by

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store