sustainable-notifications-cover.png
Lucas Cavalcante

Lucas Cavalcante

23 Nov 2020 9 min de leitura

Um sistema de notificações sustentável com react-redux-api-tools

React e Redux são uma combinação poderosa, mas precisamos ter um pouco de cuidado com ela às vezes. Essa é uma rápida história sobre como utilizei react-redux-api-tools para construir um esquema de notificações reativo que pode crescer sem se tornar vulnerável à mudanças.

Vamos começar essa publicação considerando uma arquitetura simplificada para gerenciar produtos. Veremos o que é necessário para configurar uma comunicação transparente entre React e Redux usandoo react-redux-api-tools. E, em seguida, discuturemos sobre como melhorar essa solução através de um sistema de mensageria seguindo um sólido padrão de projetos. Bora lá!

CRUD com react-redux-api-tools

Nesse artigo, usamos react-redux-api-tools para gerenciar dados e lógicas de negócio em um cenário CRUD. Para propósitos de ilustração, focaremos na CRIAÇÃO e REMOÇÃO de produtos utilizando nossa API. Essa abordagem permanece válida com pouca ou nenhuma alteração para as demais operações.

Arquitetura React-Redux

Podemos imaginar uma arquitetura bem simples. Um componente chamado DashboardPage contém dois outros componentes: ProductList e CreateProductButton, como ilustra a Figura 1. ProductList simplesmente renderiza uma lista com todos os produtos cadastrados. O CreateProductButton é uma interface para outro componente: CreateProductModal. Este, por sua vez, possui um formulário que usuários podem preencher e apertar um botão escrito “SALVAR”, que irá acionar uma requisição à API. É isto. Essa é nossa arquitetura React.

Figura 1: Arquitetura dos componentes React

Vamos olhar um pouco mais de perto a implementação de CreateProductModal.js, que descreve como usuários podem criar novos produtos. Nosso código se parecerá um pouco com essas linhas:

// CreateProductModal.js
...
import React from "react";
export class CreateProductModal extends React.Component {
  ...
  onSubmit = e => {
    ...
    this.props.createProduct(this.state.productName);
  };

  render() {
   // submit form
  }
}

Ele renderiza um formulário que, quando submetido, aciona algo parecido com um “despacho” que se conecta ao Redux. A bibilioteca react-redux-api-tools nos ajudará a intermediar esses despachos. A conexão entre componente (em um nível React) e o correspondente atualizador de estados (em um nível Redux) segue o seguinte padrão:

// CreateProductModal.js
...
import { createProduct } from "../actions";

const mapDispatchToProps = dispatch => ({
  createProduct: productName => dispatch(createProduct(productName))
});

import { connect } from "react-redux";

export default connect(null, mapDispatchToProps)(CreateProductHandler);

No lado do Redux, teremos a descrição de como a ação createProduct deve ser despachada:

// action.js
...
export const createProduct = productName => {
  const requestData = {
    method: "POST",
    headers: {
      authorization: `Token ${localStorage.token}`
    },
    body: JSON.stringify({
      name: productName
    })
  };

  return {
    types: {
      request: CREATE_PRODUCT_REQUEST,
      success: CREATE_PRODUCT_SUCCESS,
      failure: CREATE_PRODUCT_FAILURE
    },
    apiCallFunction: () => fetch(`/api/products/`, requestData)
  };
};

E também teremos o gerenciamento de estados para cada tipo de ação:

// reducer.js
...
export function productReducers(state = initialState, action) {
  switch(action.type) {
    case CREATE_PRODUCT_REQUEST:
      return {
        ...state,
      };
    case CREATE_PRODUCT_SUCCESS:
      return {
        ...state,
        productList: [...state.productList, action.response.data]
      };
    case CREATE_PRODUCT_FAILURE:
      return {
        ...state,
      };
    default:
      return state;
  }
}

Muito bom. Com isso, usuários podem efetivamente criar alguns produtos.

Comunicando a resposta da API de volta para o Componente

Acabamos de implementar uma via para que sinais que vêm da superfície React possam ser emitidos até as produndezas do armazém de estados do Redux. Veja a representação desse processo na Figura 2.

Figura 2: Sinais do React

Mas, e se desejássemos verificar, nos componentes React, se as respostas dessas requisições foram bem sucedidas, para garantirmos que a aplicação reaja de acordo? Em outras palavras, como faremos para que informações sobre o resultado de interações com a aplicação cheguem de volta até a superfície React?

Há uma série de situações que dependem, de maneira crucial, da resposta correta para esta questão. Certamente precisamos fechar o modal que contém o formulário, por exemplo. Provavelmente também precisaríamos re-desenhar a lista de produtos exibidos para exibir o novo produto que acabamos de criar. Ou, ainda, poderíamos utilizar essa informação para redirecionar o usuário a uma nova rota.

Podemos ver que o Redux, no caso de despachos realizados com sucesso, opera somente sobre a variável de estado produtctList. Nesse ponto, é intuitivo considerar: “Talvez eu possa construir alguma lógica no nível do componente para verificar se a productList sofreu alguma alteração, mesmo que pareça meio errado”. De fato, isso não seria tão prudente. Por uma série de razões, essa responsabilidade está além do escopo React, na minha opinião. Uma alternativa mais apropriada seria incluir uma nova chave no armazém do Redux. Vamos refatorar nosso manipulador de estados:

// reducer.js
...
export function productReducers(state, action) {
  switch(action.type) {
    case CREATE_PRODUCT_REQUEST:
      return {
        ...state,
        createProductIsSuccessfull: null
      };
    case CREATE_PRODUCT_SUCCESS:
      return {
        ...state,
        productList: [...state.productList, action.response.data],
        createProductIsSuccessfull: action.response.ok
      };
    case CREATE_PRODUCT_FAILURE:
      return {
        ...state,
        createProductIsSuccessfull: action.response.ok
      };
    default:
      return state;
  }
}

Dessa maneira, componentes podem se preocupar exclusivamente com como reagir a mudanças. Podemos nos aproveitar da maneira recomendada pelo próprio React para manipular o DOM quando algum estado sofre alterações. Utilizaremos um dos métodos do “ciclo de vida” de componentes chamado componentDidUpdate. Sendo assim, vamos adicionar essa lógica em CreateProductModal.js:

// CreateProductModal.js
export class CreateProductModal extends React.Component {
  ...
  componentDidUpdate(prevProps) {
    const { history, createProductIsSuccessfull } = this.props;
    if (createProductIsSuccessfull && !prevProps.createProductIsSuccessfull) {
      this.handleCloseModal();
      history.push("/dashboard-page");
    }
  }

Naturalmente, precisamos explicitamente estabelecer a conexão entre Redux e React. Para isso, basta mapear a variável de estado createProductIsSuccessfull no conjunto de props do componente et voilá.

const mapStateToProps = state => ({
  createProductIsSuccessfull: state.productReducers.createProductIsSuccessfull
});
...
export default connect(mapStateToProps, mapDispatchToProps)(CreateProductHandler);

Podemos imaginar que agora o componente está escutando o armazém do Redux, como ilustra a Figura 3. Alterações sobre esse estado específico realizadas em qualquer parte da aplicação serão detectadas de forma assíncrona por essa conexão.

Figura 3: Escutando o armazém Redux

Muito bom! Agora, quando usuários criam novos produtos, eles podem ser automaticamente redirecionados para DashboardPage. E, agora, seria muito interessante se os usuários pudessem de fato saber que deu tudo certo. Seria muito natural mostrá-los alguma notificação de sucesso como “Produto adicionado no banco de dados!”, ou algo similar.

Mensageria

DashboardPage seria um local adequado pra exibir essas notificações. Por que não no topo da página, digamos, junto com os demais componentes, como mostra a Figura 4. Poderíamos inclusive isolar essa funcionalidade em um componente especializado. Chamemos-no de Messager.

Figura 4: Incluindo o componente Messager no DashboardPage

// DashboardPage.js
...
export default class DashboardPage extends React.Component {
  render() {
    return (
      <div className="dashboardPage">
        <Messager />
        <ProductList />
        <CreateProductButton />
      </div>
    );
  }
}

Solução inicial

Inicialmente, mostrarei uma forma simples de implementar essa funcionalidade. Contudo, permita-me de antemão enfatizar que, embora essa solução não seja errada, ela carrega consigo uma forte resistência à escalabilidade. E ficará logo claro por quê.

Criaríamos uma nova chave em nosso productReducers:

// reducer.js
...
    case CREATE_PRODUCT_REQUEST:
      return {
        ...
        createProductSuccessMessage: ""
     };
    case CREATE_PRODUCT_SUCCESS:
      return {
        ...
        createProductSuccessMessage: "Product created successfully."
      };
...

E, assim, o componente Messager poderia ser escrito dessa forma:

// Messager.js
...
export class Messager extends React.Component {
  ...
  render() {
    const { createProductSuccessMessage } = this.props;
    return (
      <div>
        {createProductSuccessMessage ? (
          <Alert variant="success">
            {createProductSuccessMessage}
          </Alert>
        ) : null}
      </div>
    );
  }
}
...

Mas espere, há algo muito importante faltando em nossa implementação até agora. Não há nenhuma maneira de suprimir essas mensagens uma vez que elas sejam exibidas. Vamos solucionar isso.

Essencialmente, seria necessário limpar o conteúdo da variável de estado createProductSuccessMessage. No entanto, essa é uma responsabilidade do Redux. Portanto, precisaríamos de alguma espécie de interface no React para ativar a ação que realizaria essa limpeza. Poderíamos criar um novo método em nosso action.js:

// action.js
...
export function clearCreateProductSuccessMessage() = ({
  type: CLEAR_CREATE_PRODUCT_SUCCESS_MESSAGE
});

E, assim, poderíamos adicionar mais um tipo de ação em nosso productReducers:

// reducer.js
    ...
    case CLEAR_CREATE_PRODUCT_SUCCESS_MESSAGE:
      return {
        ...state,
        createProductSuccessMessage: ""
      };

Finalmente, refatoraríamos nosso componente Messager em um alerta do tipo dispensável:

// Messager.js
...
export class Messager extends React.Component {
  ...
  handleCloseCreateProductSuccessMessage = () => {
    const { clearCreateProductSuccessMessage } = this.props;
    clearCreateProductSuccessMessage();
  };

  render() {
    const { createProductSuccessMessage } = this.props;
    return (
      <div>
        {createProductSuccessMessage ? (
          <Alert
            variant="success"
            onClose={() => this.handleCloseCreateProductSuccessMessage()}
            dismissible
          >
            {createProductSuccessMessage}
          </Alert>
        ) : null}
      </div>
    );
  }
}
...

Ufa! Agora temos tudo o que precisamos. Como prometi: super simples. Mas essa simplicidade não se sustentaria por muito tempo caso desejássemos notificar o sucesso de qualquer outra operação em nossa aplicação. Seja EDITAR, REMOVER, or MODIFICAR um produto, seja alguma operação relacionada a outro modelo. Quando precisarmos utilizar Messager para notificar mensagens em DashboardPage, deveremos repetir todo este ritual novamente e logo nosso código “explodirá” em linhas e linhas. Vejamos o caso de REMOÇÃO de produtos.

Explosão de código

Do lado do Redux, teríamos que descrever como a ação deleteProduct deve ser despachada. E, aqui, vamos aproveitar a oportunidade para implementar também a ação que limpa os valores da chave deleteProductSuccessMessage.

// action.js
...
export const deleteProduct = productId => {
  const requestData = {
    method: "DELETE",
    headers: {
      authorization: `Token ${localStorage.token}`
    }
  };
  return {
    types: {
      request: DELETE_PRODUCT_REQUEST,
      success: DELETE_PRODUCT_SUCCESS,
      failure: DELETE_PRODUCT_FAILURE
    },
    extraData: {
      productId
    },
    apiCallFunction: () => fetch(`/api/products/${productId}/`, requestData)
  };
};

export function clearDeleteProductSuccessMessage() = ({
  type: CLEAR_DELETE_PRODUCT_SUCCESS_MESSAGE
});

Também precisaríamos atualizar nosso productReducers:

// reducer.js
...
    case DELETE_PRODUCT_REQUEST:
      return {
        ...state,
        deleteProductIsSuccessfull: null,
        deleteProductSuccessMessage: ""
     };
    case DELETE_PRODUCT_SUCCESS:
      const productListAfterDelete = [...state.todoLists];
      productListAfterDelete.splice(action.extraData.productId, 1);
      return {
        ...state,
        productList: productListAfterDelete,
        deleteProductIsSuccessfull: action.response.ok,
        deleteProductSuccessMessage: "Product deleted successfully."
      };
    case DELETE_PRODUCT_FAILURE:
      return {
        ...state,
        deleteProductIsSuccessfull: action.response.ok
      };
    case CLEAR_DELETE_PRODUCT_SUCCESS_MESSAGE:
      return {
        ...state,
        deleteProductSuccessMessage: ""
      };
// ...

Agora, no nível do React, precisaríamos incluir as seguintes linhas no nosso componente Messager:

// Messager.js
...
export class Messager extends React.Component {
  ...
  handleCloseCreateProductSuccessMessage = () => {
    const { clearCreateProductSuccessMessage } = this.props;
    clearCreateProductSuccessMessage();
  };

  handleCloseDeleteProductSuccessMessage = () => {
    const { clearDeleteProductSuccessMessage } = this.props;
    clearDeleteProductSuccessMessage();
  };

  render() {
    const { deleteProductSuccessMessage } = this.props;
    return (
      <div>
        {createProductSuccessMessage ? (
          <Alert
            variant="success"
            onClose={() => this.handleCloseCreateProductSuccessMessage()}
            dismissible
          >
            {createProductSuccessMessage}
          </Alert>
        ) : null}
        {deleteProductSuccessMessage ? (
          <Alert
            variant="success"
            onClose={() => this.handleCloseDeleteProductSuccessMessage()}
            dismissible
          >
            {deleteProductSuccessMessage}
          </Alert>
        ) : null}
      </div>
    );
  }
}
...

Minha nossa. Você consegue sentir esse cheiro? Isso mesmo. Esse é precisamente o ponto em que nossa arquitetura super simples começa a apodrecer e se tornar código legado.

Desarmando a bomba

Há uma solução que poderia imediatamente aliviar essa explosão de código. Poderíamos converter, em um único estado, a lista de todas as mensagens a serem exibidas. Assim, não mais teríamos um estado para cada mensagem distinta e evitaríamos qualquer menção à variáveis como XSuccessMessage. Ao invés disso, utilizaríamos um buffer chamado messageList para armazenar qualquer tipo de mensagem que venha a surgir.

Gostaríamos de ter algo parecido com o esquema da Figura 5, onde três componentes distintos comunicam-se entre si através de eventos. Essa é, na verdade, uma abstração bastante difundida que está associada a uma série de benefícios, nesse contexto. Ela favorece uma aplicação em que suas partes estão desacopladas entre si e que, ao mesmo tempo, cada parte pode se comunicar com todas as demais de maneira assíncrona.

Figura 5: Padrão de projeto de mensageria

Então vamos remover todos os estados associados a “messagens” de nosso reducer.js, como createProductSuccessMessage e deleteProductSuccessMessage. Podemos também mover métodos como clearCreateProductSuccessMessage e clearDeleteProductSuccessMessage para um diretório específico, como messager/action.js. E, em um novo arquivo messager/reducer.js, teremos algo como:

// messager/reducer.js
...
    case CREATE_PRODUCT_SUCCESS:
      return {
        ...state,
        messageList: [...state.messageList, "Product created successfully."]
      };
    case DELETE_PRODUCT_SUCCESS:
      return {
        ...state,
        messageList: [...state.messageList, "Product deleted successfully."]
     };
...

Assim, podemos refatorar nosso componente Messager.js.

// Messager.js
...
  handleCloseMessage = index => {
    const { clearSuccessMessage } = this.props;
    clearSuccessMessage(index);
  };
  render() {
    const { messageList } = this.props;
    return (
      <div>
        {messageList.map((message, index) => (
          <Alert
            variant="success"
            onClose={() => this.handleCloseMessage(index)}
            dismissible
            key={`Message: ${message.id}`}
          >
            {message}
          </Alert>
        ))}
      </div>
    );
  }
}
...

É importante notar que agora temos uma única ação para limpar as notificações armazenadas em nosso buffer: clearSuccessMessage, que é a última coisa que falta implementarmos. Uma vez que clearSuccessMessage for ativado, recebendo o index de qual mensagem dever ser apagada, um simples despacho como esse:

// messager/action.js
export const clearSuccessMessage = index => ({
  type: CLEAR_SUCCESS_MESSAGE,
  extraData: {
    index
  }
});

poderia ativar o seguinte comportamento do Redux:

    case CLEAR_SUCCESS_MESSAGE:
      const newMessageList = [...state.messageList];
      newMessageList.splice(action.extraData.index, 1);
      return {
        ...state,
        messageList: newMessageList
      };

Dessa forma, separamos qualquer ação e despacho relacionados à mensageria em um módulo isolado, como ilustra a Figura 6. E é isto! A bomba foi desarmada. Estamos seguros, por hora.

Figura 6: Um módulo isolado para lidar com Messager

Conclusões

Nessa publicação, vimos como construir uma eficiente arquitetura para um sistema de mensageria em uma aplicação React-Redux. Refletimos sobre armadilhas comuns que podem surgir quando tentamos amarrar componentes com variáveis de estados compartilhadas. E, finalmente, implementamos um tipo de solução que é robusto para crescer. Aprendemos que podemos abstrair a responsabilidade de comunicação de componentes individuais. Com esse isolamento, podemos evitar duplicação de código que poderia congelar nosso código de forma severa.

Quais falhas você enxerga nessa solução? Como você acha que podemos melhorá-la? Escreva-nos!