Usamos seletores redux demais

Quando olho para o arquivo {domain} /selectors.js nos grandes projetos React / Redux com os quais trabalho, muitas vezes vejo uma lista enorme de seletores de redux desse tipo:


getUsers(state) getUser(id)(state) getUserId(id)(state) getUserFirstName(id)(state) getUserLastName(id)(state) getUserEmailSelector(id)(state) getUserFullName(id)(state) … 

À primeira vista, o uso de seletores não parece incomum, mas com a experiência começamos a entender que pode haver muitos seletores. E parece que sobrevivemos a esse ponto.


imagem

Redux e seletores


Vamos dar uma olhada no Redux. O que ele é, por quê? Após ler o redux.js.org, entendemos que o Redux é um "contêiner previsível para armazenar o estado do aplicativo JavaScript"


Ao usar o Redux, é recomendável usar seletores, mesmo que sejam opcionais. Os seletores são apenas getters para obter algumas partes de todo o estado, ou seja, funções do formulário (State) => SubState . Normalmente, escrevemos seletores para não acessar diretamente o estado e, em seguida, podemos combinar ou memorizar os resultados desses seletores. Parece razoável.


Profundamente imerso em seletores


A lista de seletores que citei na introdução deste artigo é característica do código criado às pressas.


Imagine que temos um modelo de usuário e queremos adicionar um novo campo de email a ele. Temos um componente que esperava que firstName e lastName fossem inseridos e agora ele aguardará outro email . Seguindo a lógica do código com os seletores, introduzindo um novo campo de email, o autor deve adicionar o seletor getUserEmailSelector e usá-lo para passar esse campo ao componente. Bingo!


Mas é o bingo? E se conseguirmos outro seletor, o que será mais complicado? Vamos combiná-lo com outros seletores e, talvez, chegaremos a esta foto:


 const getUsers = (state) => state.users; const getUser = (id) => (state) => getUsers(state)[id]; const getUserEmailSelector = (id) => (state) => getUser(id)(state).email; 

Surge a primeira pergunta: o que o seletor getUserEmailSelector deve retornar se o seletor getUser retornar undefined ? E essa é uma situação provável - bugs, refatoração, legado - tudo pode levar a isso. De um modo geral, nunca é tarefa dos seletores manipular erros ou fornecer valores padrão.


O segundo problema surge com o teste desses seletores. Se quisermos cobri-los com testes de unidade, precisaremos de dados simulados idênticos aos da produção. Teremos que usar os dados simulados de todo o estado (já que o estado não pode ser não consistente na produção) apenas para esse seletor. Isso, dependendo da arquitetura do nosso aplicativo, pode ser muito inconveniente - arrastando dados para testes.


Vamos supor que escrevemos e testamos o seletor getUserEmailSelector conforme descrito acima. Nós o usamos e conectamos o componente ao estado:


 const mapStateToProps = (state, ownProps) => ({ firstName: getUserFirstName(ownProps.userId)(state), lastName: getUserLastName(ownProps.userId)(state), //   email: getUserEmailName(ownProps.userId)(state), }) 

Seguindo a lógica acima, obtivemos um monte de seletores que estavam no início do artigo.
Nós fomos longe demais. Como resultado, escrevemos uma pseudo-API para a entidade Usuário. Esta API não pode ser usada fora do contexto do Redux porque requer uma conversão de estado completa. Além disso, é difícil estender essa API - ao adicionar novos campos à entidade Usuário, devemos criar novos seletores, adicioná-los ao mapStateToProps, escrever mais código padrão.


Ou talvez você deva acessar diretamente os campos da entidade?


Se o problema é apenas o fato de termos muitos seletores - talvez apenas usemos getUser e acessemos diretamente as propriedades da entidade?


 const user = getUser(id)(state); const email = user.email; 

Essa abordagem resolve o problema de escrever e oferecer suporte a um grande número de seletores, mas cria outro problema. Se precisarmos alterar o modelo do usuário, também precisaremos controlar todos os locais onde o user.email é user.email ( nota do tradutor ou outro campo que user.email ). Com uma grande quantidade de código no projeto, isso pode se tornar uma tarefa difícil e complicar até um pouco de refatoração. Quando tínhamos um seletor, ele nos protegia de tais consequências de mudanças, porque assumiu a responsabilidade de trabalhar com o modelo e o código usando o seletor não sabia nada sobre o modelo.


O acesso direto é compreensível. Mas e quanto a receber dados calculados? Por exemplo, com o nome de usuário completo, que é uma concatenação do nome e do sobrenome? Precisa cavar mais ...


imagem

O modelo de domínio é direcionado. Redux - Secundário


Você pode chegar a esta figura respondendo a duas perguntas:


  • Como definimos nosso modelo de domínio?
  • Como vamos armazenar os dados? (gerenciamento de estado, para isso usamos redux * nota do tradutor * de que a camada de persistência é chamada em DDD)

Respondendo à pergunta "Como definimos o modelo de domínio" (no nosso caso, Usuário), vamos abstrair do redux e decidir o que é um "usuário" e qual API é necessária para interagir com ele?


 // api.ts type User = { id: string, firstName: string, lastName: string, email: string, ... } const getFirstName = (user: User) => user.firstName; const getLastName = (user: User) => user.lastName; const getFullName = (user: User) => `${user.firstName} ${user.lastName}`; const getEmail = (user: User) => user.email; ... const createUser = (id: string, firstName: string, ...) => User; 

Será bom se sempre usarmos essa API e considerarmos o modelo de Usuário inacessível fora do arquivo api.ts. Isso significa que nunca voltaremos diretamente para os campos da entidade, pois o código que usa a API nem sabe qual entidade possui campos.


Agora podemos voltar ao Redux e resolver problemas relacionados apenas ao estado:


  • Que lugar os usuários ocupam em nosso artigo?
  • Como devemos armazenar usuários? Uma lista? Dicionário (valor-chave)? De que outra forma?
  • Como obteremos uma instância de usuário do estado? A memorização deve ser usada? (no contexto do seletor getUser)

API pequena com grandes benefícios


Aplicando o princípio de compartilhar responsabilidades entre a área de estudo e o estado, recebemos muitos bônus.


Um modelo de domínio bem documentado (modelo de usuário e sua API) no arquivo api.ts. Ele se presta bem a testes, pois não tem dependências. Podemos extrair o modelo e a API na biblioteca para reutilização em outros aplicativos.


Podemos combinar facilmente as funções da API como seletores, o que é uma vantagem incomparável sobre o acesso direto às propriedades. Além disso, nossa interface de dados agora é fácil de manter no futuro - podemos alterar facilmente o modelo do usuário sem alterar o código que o utiliza.


Nenhuma mágica aconteceu com a API, ainda parece clara. A API se assemelha ao que foi feito usando seletores, mas tem uma diferença importante: não precisa de todo o estado, não precisa mais suportar o estado completo do aplicativo para teste - a API não tem nada a ver com Redux e seu código padrão.


Os suportes dos componentes ficaram mais limpos. Em vez de aguardar a entrada das propriedades firstName, lastName e email, o componente recebe uma instância de User e usa internamente sua API para acessar os dados necessários. Acontece que precisamos de apenas um seletor - getUser.


Existem benefícios para redutores e middleware dessa API. A essência do benefício é que você pode primeiro obter uma instância do Usuário, lidar com os valores ausentes, processar ou impedir todos os erros e, em seguida, usar os métodos da API. É melhor do que usar cada campo individual usando seletores isolados da área de assunto. Assim, o Redux realmente se torna um “recipiente previsível” e deixa de ser um objeto “divino” com o conhecimento de tudo.


Conclusão


Com boas intenções (leia aqui - seletores), o caminho para o inferno é pavimentado: não queríamos acessar os campos da entidade diretamente e fizemos seletores separados para isso.


Embora a ideia dos próprios seletores seja boa, seu uso excessivo dificulta a manutenção do nosso código.


A solução descrita no artigo propõe resolver o problema em dois estágios - primeiro descreva o modelo de domínio e sua API, depois lide com o Redux (armazenamento de dados, seletores). Dessa forma, você escreverá códigos melhores e menores - você só precisa de um seletor para criar uma API mais flexível e escalável.


Notas do tradutor


  1. Eu usei a palavra estado, pois parece que ela entrou firmemente no vocabulário dos desenvolvedores de língua russa.
  2. O autor usa as palavras upstream / downstream para significar "código de alto nível / baixo nível" (se de acordo com Martin) ou "código usado abaixo / código abaixo que usa o que está escrito acima", mas não é correto descobrir como usá-lo na tradução Eu poderia, portanto, me consolar tentando não perturbar o senso geral.

Terei prazer em aceitar comentários e sugestões de correções no PM e corrigi-los.

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


All Articles