Refatorando código legado em projetos React — Parte IV

Bruno Nardini
Aug 2, 2019 · 8 min read
Fotografia disponibilizada por Pixabay

O Redux é um contêiner de estado previsível e ajuda a manter a consistência do comportamento de uma aplicação. Sua utilização em projetos junto ao React se tornou tão popular que os desenvolvedores iniciantes em React passaram a usá-lo por obrigação, ignorando seu custo e efeitos colaterais.

Seu uso indiscriminado foi tão grande que o Dan Abramov, coautor do Redux, escreveu o artigo “You Might Not Need Redux” para alertar que a decisão de usá-lo deve ser planejada e justificada.

Faz parte da evolução de um software reavaliar a decisão de cada dependência existente, então remover ou trocar uma biblioteca pode fazer parte de uma refatoração. Este processo não precisa ser feito de uma vez só, pode e deve ser feito de forma gradativa.

Neste artigo continua a implementação da funcionalidade proposta na Parte III desta série, para fins didáticos iremos manter o Redux e abordar cada um de seus conceitos durante a refatoração e implementação desta nova funcionalidade.

Redux

A arquitetura do Redux gira em torno de um fluxo de dados unidirecional estrito, com um único padrão de ciclo de vida, tornando a lógica da aplicação mais previsível e mais fácil de entender.

Na figura abaixo está o fluxo do Redux integrado com o React:

Figura 1 — Fluxo do Redux

No projeto de exemplo é utilizado a biblioteca React Redux para fazer a conexão do React com o Redux. Para entender como funciona essa conexão é necessário conhecer conceitos avançados do React como context e higher-order component, mas sua utilização é bem simples, basta definir o provider como foi feito no componente App e depois usar o connect()() no componente que quiser acessar o estado da store ou para disparar uma ação.

Primeiro os dados

Antes de sair alterando o Redux é necessário avaliar quais são os dados que serão trabalhados, então para fazer esta análise é feito três perguntas:

  1. Qual a origem desses dados? São retornados por uma API? É uma entrada feita pelo usuário?
  2. Qual o destino desses dados? Será usado para mostrar informações ao usuário? Será enviado para uma API?
  3. Como esses dados serão tratados e armazenados? Esta pergunta é a mais importante e precisa da resposta das perguntas anteriores, pois a origem e o destino é o que define como os dados serão organizados.

Neste exemplo os dados serão obtidos através de uma API com um contrato já definido, então a origem parte de uma integração. Para poder testar o código sem fazer a integração, será usado uma cópia do retorno da API com valores diversos que ficará no arquivo /client/mocks/lectures.json com o conteúdo abaixo:

Esses dados serão usados para montar uma lista única que será exibida na página inicial. Além de unir as duas listas em uma lista só, será necessário transformar o objeto na forma das propriedades do componente.

Aproveitando o padrão já criado no projeto, pode ser criado funções (adapters) para transformar os modelos session e workshop no formato (view model) que o CourseCard espera. Começaremos com os testes no arquivo /client/src/adapters/lectureAdapter.test.js:

O lectures.json foi usado aqui para garantir o contrato da API.

Em seguida é implementado as funções no arquivo /client/src/adapters/lectureAdapter.js:

A utilização destas funções serão abordadas no próximo tópico.

Action Creator

Ao montar o componente HomePage é disparado a ação "GET_COURSES" para store do Redux através do mapDispatchToProps, que poderia estar assim:

Mas se encontra assim:

O onGetCourses é um action creator, que é uma função que encapsula a construção do objeto que representa a ação:

Esse encapsulamento oferece várias vantagens:

  • Coloca a lógica para criação da ação em um só lugar, facilitando a manutenção e extensão.
  • Se estiverem bem organizados, facilita o mapeamento de todas as ações e deixa bem mais claro o fluxo da aplicação.
  • Pode concentrar toda lógica de transformação e construção dos dados a serem utilizados na ação.
  • Por serem funções pura (plain functions), torna-se bem mais fácil de criar testes de unidade eficientes.

Para a nova funcionalidade serão criadas três ações:

  • GET_LECTURES é a ação inicial que irá disparar a requisição à API.
  • GET_LECTURES_SUCCESS para o retorno com sucesso da API. No action creator desta ação será feito a transformação do retorno da API para o modelo final que será usado no componente, desta forma a transformação é feita apenas uma única vez a cada requisição.
  • GET_LECTURES_FAIL para o caso ocorra um erro na integração.

Como sempre, começaremos com o teste para cada action creator dessas ações, então crie o arquivo actions.test.js no diretório /client/src/store com os seguintes testes:

O uso da constante para validar o action type dá a segurança que a constante foi usada, então se o action creator retornar um action type com um valor diferente do que está na constante faria quebrar o teste, e ao mesmo tempo que ao mudar o valor da constante não faria o teste quebrar, que é um dos benefícios de usar uma constate: poder trocar o valor em um lugar só e replicar para todos os lugares que são usados. Ou seja, mudar algo que não quebra a aplicação não deveria quebrar o teste.

Em seguida faça a implementação no arquivo actions.js que está no mesmo diretório:

O novo action creator pode agora ser usado no componente HomePage:

Repare que a lógica da ação foi abstraída, o componente só sabe que precisa executar a função onGetLectures(), o restante ficou como responsabilidade do action creator. A função onGetLectures é mapeado para propriedade loadLectures do componente HomePage, que por sua vez é usado no método componentDidMount(), ou seja, será requisitado a lista após o componente fazer sua primeira renderização.

Ações assíncronas

O próximo passo é fazer que a ação "GET_LECTURES" faça uma requisição a API, porém, para executar qualquer código assíncrono no Redux deve ser feito através de um middleware. Neste exemplo é usado a biblioteca Redux Saga como um middleware do Redux para tratar estes processos assíncronos.

As sagas estão centralizadas no arquivo /client/src/store/sagas.js e são exportadas pelo generator sagas:

É utilizado uma funcionalidade do ES6 chamado generator function para fazer o fluxo assíncrono mais fácil de ler, escrever e testar. Cada ação disparar um novo generator através da função takeLatest() e alinhadas para execução paralela pela função all().

Para criar o teste da saga teremos que substituir a integração com a API com um test double (vide Parte III) para evitar a integração durante o teste. Para isso, será usada a biblioteca Sinon para substituir a conexão HTTP feita pelo Axios. Faça a sua instalação com o comando abaixo no /client:

npm install --save-dev sinon

Agora é possível testar a saga com teste de unidade:

Em cada teste substitui a implementação do get() do Axios usando o Sinon através da função sinon.stub(axios, "get"). No primeiro teste é definido um retorno fixo através do método callsFake() e no segundo é definido que será lançado uma exceção através do método throws(). No final de cada teste, através do afterEach(), é restaurado o estado inicial do método get() do Axios através do comando axios.get.restore() fornecido pelo Sinon.

Foi utilizado a função utilitária runSata() do próprio Redux Saga para executar a saga como uma promessa. Todas as ações disparadas pela saga é armazenada no array dispatched que é utilizado posteriormente para validar o teste.

Em seguida vem a implementação da nova saga onGetLectures():

Implementar e testar sagas, por mais que pareça ser simples, é sempre um desafio até para os mais experientes desenvolvedores, então é uma boa prática deixá-las bem enxutas, sem regras de negócio. No exemplo acima a regra é abstraída pelo service ApiService para a integração com a API e os action creators onGetLecturesSuccess() e onGetLecturesFail(), abordados no tópico anterior, para o tratamento do retorno da API.

Como ApiService é exclusivo para integração, faz mais sentido ser testado no teste de integração, o método novo getLectures() é tão enxuto que é escrito em uma só linha:

getLectures: () => axios.get(`${baseURL}/lectures`)

Reducers

Reducers especificam como o estado da aplicação é alterado em resposta às ações enviadas para a store.

Neste exemplo, é necessário tratar as ações "GET_LECTURES_SUCCES" e "GET_LECTURES_FAIL" que são disparadas pela saga criada no tópico anterior, que podem ser testados da seguinte forma:

No client/src/store/reducers.js é definido o estado inicial para as novas propriedades:

  • lectures: [] para armazenar a lista de lições.
  • lecturesError: null para armazenar informações de erro caso a saga não consiga obter a lista de lições da API.

Em seguida é tratado cada ação para preencher estas propriedades:

As regras de transformações que ficaram nos action creators poderiam muito bem ficar no reducer, ambos os casos são bem adotados e considerados boa prática. Particularmente prefiro deixar a lógica das transformações para os actions creatores e selectors, e deixar a lógica de armazenagem nos reducers, acho que fica mais organizado, mas já vi muitos projetos com toda lógica centralizada nos reducers e estavam muito bem organizados, então ambos os casos são bons caminhos.

No caso de reducers complexos ou se o estado estiver muito grande, é importante usar o combineReducers para separar os reducers em módulos para ficar mais organizado.

Selectors

O selector é uma função que encapsula a lógica de leitura do estado. Enquanto o action creator se preocupa com a entrada da store, o selector se preocupa com a saída.

Neste exemplo não será usado pois não há processamento dos dados após serem armazenados. Mas seria muito útil em casos como o uso de filtros e buscas que são processados na própria aplicação front-end.

Assim como o action creator, o selector também separa a responsabilidade de manipular a store dos componentes, facilitando a reutilização e manutenção do projeto.

Fim do ciclo

Depois dessa jornada pelos conceitos do Redux, o ciclo termina onde começou: no componente.

No HomePage.test.js entra novos testes para a nova lista:

E depois no HomePage.js entra a implementação:

Como toda a parte pesada da lógica ficou por conta do Redux, a implementação no componente ficou bem simples, bastou mapear propriedades do estado lectures e lecturesError para a propriedade do componente, e no render() valida a existência do lecturesError, se existir então exibe a mensagem do erro, senão exibe a lista que está em lectures.

O resultado final, com a API retornando 404 por ainda não ter implementado a nova rota, fica assim:

Desta forma o projeto pode ficar disponível online e quando a API estiver pronta, a página inicial vai exibir a lista de eventos e palestras. Os testes que fizemos garante que tudo irá funcionar.

O mundo real

Com a conclusão desta Parte IV foram abordados várias casos que aparecem constantemente em projetos React do mundo real. É possível sair de um projeto com código legado para um projeto com alta qualidade, por mais complexo que ele seja, e ter segurança para evoluí-lo.

Os gists (códigos) ficaram um pouco grande, mas projetos reais são assim, principalmente projetos com código legado e aquele delicioso spaghetti code, então nada mais justo que investir o seu tempo nesta jornada com muito aprendizado e conseguir realmente por em prática esse conhecimento no seu dia a dia.

Espero que tenha gostado de codificar comigo, caso você tenha alguma dúvida, ou tenha uma situação diferente em seu projeto, ou só queria discutir algum assunto relacionado, deixe seu comentário.

Na próxima e última parte serão abordados alguns mitos e verdades sobre código legado, refatoração e testes para fechar este assunto com chave de ouro.

M4U Tech

Somos uma empresa de pessoas fantásticas que desenvolvem soluções de pagamentos.

M4U Tech

Somos uma empresa de pessoas fantásticas que desenvolvem soluções de pagamentos.

Bruno Nardini

Written by

Tech Lead at RD Station | Teacher NardiniAcademy.com | Blogger BrunoNardini.com | Husband & Father | Guitar Player | Build and teach the WEB

M4U Tech

Somos uma empresa de pessoas fantásticas que desenvolvem soluções de pagamentos.