Notificações, Toast, React e Redux

A missão era simples: enviar uma mensagem não bloqueante de sucesso ou erro em resposta a uma requisição assíncrona. Seria simples se não fosse pelas palavras em negrito, pois me deparei com várias questões de arquitetura do projeto que está em React com Redux, e ao pesquisar a melhor solução eu vi vários artigos em blog inconformados com o problema e dando suas sugestões de como deveria ficar, só que nenhuma dessas soluções me deixou confortável. Então neste artigo eu trago uma alternativa, expondo passo a passo as questões levantadas e as decisões tomadas para implementar essa solução.

Toasts

Toast é um mensagem de notificação (pop-up notification) que ganhou esse apelido por parecer uma torrada saltando na tela. Eu trago o costumo de chamar essas mensagens de toast por causa do Android, que na documentação descreve como:

“A toast provides simple feedback about an operation in a small popup.”

A grande vantagem do toast é o fato de não ser bloqueante, ele é exibido em algum canto da tela e não impede o usuário de continuar usando o sistema/site/app.

React-Toastify

Definido que seria um toast, restava procurar uma biblioteca com a implementação já pronta. Em uma busca (não tão longa assim) por uma, encontrei o React-Toastify, que já é para React e é bem flexível para ser adaptada às minhas necessidades de layout.

Encontrei outras alterantivas, mas sem adaptação pronta para React:

Colocando em prática

Definido a biblioteca, era hora de começar a implementar a solução. Conforme especificado na documentação, basta fazer a importação do css ReactToastify.min.css e adicionar o ToastContainer ao seu container principal e está pronto para a chamada do toast. Depois seria só chamar o toast, como toast('Lorem ipsum dolor'); por exemplo, mas como eu falei no começo do artigo, essa mensagem seria em resposta a uma requisição assíncrona, então agora que se inicia o problema…

Container?

Como toast é uma uma interface, o mais intuitivo é colocar a sua chamada dentro de um container (componente React). Vamos imaginar o seguinte cenário:

Cenário de exemplo:
O usuário clica no botão de excluir um registro da tabela. Esse botão dispara a ação (Redux action) “DELETE”, que irá chamar uma API para executar a operação, que por sua vez irá disparar a ação “DELETE_SUCCESS” ou “DELETE_FAIL” de acordo com a resposta da API, sendo que ambas ações de retorno disparam um toast para o usuário.

Neste exemplo, o toast não pode ser exibido no click no botão, deve aguardar a resposta da API para saber se será exibido uma mensagem de sucesso ou de erro. O container não escuta ações, este papel é do reducer, então as ações de retorno poderiam alterar uma flag na store para indicar o resultado, algo como deleteStatus e o container iria chamar o toast de sucesso ao valor 'success' ou de erro com o valor 'error'. Só que depois de chamar de chamar o Toast, o deleteStatus deveria ser reiniciado, para evitar o Toast ser chamado mais de uma vez. Começou a ficar complicado. Dessa forma teria que ter outras ações só para controlar o estado do toast, e isso causaria o que eu chamo de redux-actions-hell, que são ações que não deveriam existir e polui o código.

Além de criar um estado e ação desnecessária, eu teria que ficar tratando isso a cada chamada de toast, gerando muito código desnecessário e suscetível a bugs.

Saga?

O Redux-Saga é uma excelente biblioteca que ajuda a tratar os efeitos colaterais de ações assíncronas no Redux, e no projeto em questão foi utilizado para fazer conexões com a API. Usando o exemplo, o código da saga ficaria assim:

function* delete(action) {
  const { payload } = action;
 
  try {
    yield MyService.delete(payload);
 
    yield put({
      type: 'DELETE_SUCCESS',
    });
  } catch (error) {
    yield put({
      type: 'DELETE_FAIL',
      error,
    });
  }
}

Como é aqui que eu defino se é sucesso ou falha, o intuitivo seria chamar o Toast aqui. Até iria funcionar, pois bastaria invocar o toast('Operação realizado com sucesso!'); que o toast seria exibido e não seria criado o redux-actions-hell que citei no tópico anterior. Porém, isso geraria vários problemas do qual quero evitar.

O mais grave são os problemas de responsabilidade e testabilidade. A saga deveria ser simples e fácil de testar, pois a responsabilidade dela é só transitar ações em uma chama assíncrona, ele não deveria ter operações de telas. Como mencionei no tópico anterior, toast é uma operação de interface. Então além de tomar responsabilidade a qual não pertence, dificulta o teste da saga, sendo necessário criar um proxy para conseguir testar a saga sem chamar a biblioteca de terceiro.

Outro problema seria a pulverização da chamada do toast em várias sagas. Por mais que fizesse um encapsulamento da chama à biblioteca, esse acoplamento iria dificultar a manutenção.

Middleware!

Depois de ter eliminado o container e saga, eu confesso que fiquei perdido, pois todas as soluções violava uma teoria da arquitetura. Depois de muito pensar e reler várias vezes a documentação do Redux, eu lembrei que a saga em si não é nativo, é um middleware, pois ele é um hook do fluxo redux, que faz todo sentido para o toast, pois a mensagem é um efeito colateral de uma ação, e usando middleware ele fica completamente isolado, testável e reutilizável.

Na prática, eu teria que identificar que a ação deseja disparar um toast. Eu até poderia criar um tipo de ação como “SHOW_TOAST” e chamá-la na saga, mas o toast é o efeito colateral de uma ação, não a ação em si, então o middleware teria que identificar por outra propriedade da ação, que já poderia servir para definir as próprias propriedades do toast.

A implementação, adaptada para este artigo, ficou assim:

import { toast } from 'react-toastify';
 
const toastMiddleware = () => next => (action) => {
  if (action.toast) {
    toast(action.toast.message, action.toast.options);
  }
  return next(action);
};
 
export default toastMiddleware;

Você já notou que a propriedade de controle foi nomeada como “toast”, para ser o mais óbvio possível. E para criar a ação do toast criei o arquivo “toastActionCreator.jsx” com o código:

export const ToastTypes = {
  default: 'default',
  info: 'info',
  success: 'success',
  warning: 'warning',
  error: 'error',
};
 
export const buildToast = (message, type = 'default', options) => ({
  message,
  type,
  options,
});
 
export default (message, type = 'default', options) => ({
  type: 'SHOW_TOAST',
  toast: buildToast(message, type, options),
});

O “toastActionCreator” cria a ação completa, para caso haja necessidade de chamar apenas o toast, mas como já mencionei algumas vezes, o toast como regra é um efeito de uma ação, como demonstro no exemplo atualizado:

import { buildToast, ToastTypes } from '../toastActionCreator';
 
function* delete(action) {
  const { payload } = action;
 
  try {
    yield MyService.delete(payload);
 
    yield put({
      type: 'DELETE_SUCCESS',
      toast: buildToast('Operação realizada com sucesso.', ToastTypes.success),
    });
  } catch (error) {
    yield put({
      type: 'DELETE_FAIL',
      toast: buildToast('Erro ao executar operação!', ToastTypes.error),
      error,
    });
  }
}

Se você acompanha a timeline do redux, você veria a ação assim:

{
  type: 'DELETE_SUCCESS',
  toast: {
    message: 'Operação realizada com sucesso.',
    type: 'success',
    options: undefined,
  },
}

Assim fica mais fácil de testar a saga, pois a saga continuou trabalhando com objetos simples (plain objects), além de ficar flexível, pois você pode passar propriedades para o toast via “action”, como o texto com tradução, entre outras.

Qualquer tratamento de layout deve ser implementado direto no middleware, para deixar as ações bem limpas.

Conclusão

Neste artigo eu tratei não só a implementação do toast, mas também vários conceitos importantes do Redux, demonstrando passo a passo meu raciocínio durante o processo de implementação. O objetivo é expor meu ponto de vista para promover o debate, pois são paradigmas relativamente novos, e nada melhor do que a troca de experiência para promover a evolução da tecnologia. E o que você achou? Como faria diferente?

Deixe um comentário