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.

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 ...

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
- Eu usei a palavra estado, pois parece que ela entrou firmemente no vocabulário dos desenvolvedores de língua russa.
- 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.