Cuando miro el archivo {dominio} /selectors.js en los grandes proyectos React / Redux con los que estoy trabajando, a menudo veo una gran lista de selectores redux de este tipo:
getUsers(state) getUser(id)(state) getUserId(id)(state) getUserFirstName(id)(state) getUserLastName(id)(state) getUserEmailSelector(id)(state) getUserFullName(id)(state) …
A primera vista, el uso de selectores no parece inusual, pero con la experiencia comenzamos a comprender que puede haber demasiados selectores. Y parece que hemos sobrevivido hasta este punto.

Redux y selectores
Miremos a Redux. ¿Qué es él, por qué? Después de leer redux.js.org, entendemos que Redux es un "contenedor predecible para almacenar el estado de la aplicación JavaScript"
Cuando use Redux, se recomienda usar selectores, incluso si son opcionales. Los selectores son solo captadores para obtener algunas partes de todo el estado, es decir funciones de la forma (State) => SubState
. Por lo general, escribimos selectores para no acceder directamente al estado y luego podemos combinar o memorizar los resultados de estos selectores. Suena razonable
Profundamente inmerso en selectores
La lista de selectores que cité en la introducción de este artículo es característica del código creado a toda prisa.
Imagine que tenemos un modelo de Usuario y queremos agregarle un nuevo campo de correo electrónico. Tenemos un componente que esperaba que se firstName
y lastName
, y ahora esperará otro email
. Siguiendo la lógica en el código con los selectores, introduciendo un nuevo campo de correo electrónico, el autor debe agregar el selector getUserEmailSelector
y usarlo para pasar este campo al componente. Bingo!
¿Pero es bingo? Y si tenemos otro selector, ¿cuál será más complicado? Lo combinaremos con otros selectores, y quizás lleguemos a esta imagen:
const getUsers = (state) => state.users; const getUser = (id) => (state) => getUsers(state)[id]; const getUserEmailSelector = (id) => (state) => getUser(id)(state).email;
Surge la primera pregunta: ¿qué debería getUserEmailSelector
selector getUserEmailSelector
si el selector getUser
regresa undefined
? Y esta es una situación probable: errores, refactorización, legado, todo puede conducir a eso. En términos generales, nunca es tarea de los selectores manejar errores o proporcionar valores predeterminados.
El segundo problema surge al probar tales selectores. Si queremos cubrirlos con pruebas unitarias, necesitaremos datos simulados idénticos a los datos de producción. Tendremos que usar los datos simulados de todo el estado (ya que el estado no puede ser inconsistente en la producción) solo para este selector. Esto, dependiendo de la arquitectura de nuestra aplicación, puede ser muy inconveniente: arrastrar datos a las pruebas.
Supongamos que escribimos y probamos el selector getUserEmailSelector
como se describió anteriormente. Lo usamos y conectamos el componente al estado:
const mapStateToProps = (state, ownProps) => ({ firstName: getUserFirstName(ownProps.userId)(state), lastName: getUserLastName(ownProps.userId)(state), // email: getUserEmailName(ownProps.userId)(state), })
Siguiendo la lógica anterior, obtuvimos ese grupo de selectores que estaban al comienzo del artículo.
Hemos ido demasiado lejos. Como resultado, escribimos una pseudo API para la entidad Usuario. Esta API no se puede usar fuera del contexto de Redux porque requiere una conversión de estado completa. Además, esta API es difícil de ampliar: al agregar nuevos campos a la entidad Usuario, debemos crear nuevos selectores, agregarlos a mapStateToProps, escribir más código repetitivo.
¿O tal vez debería acceder a los campos de entidad directamente?
Si el problema es solo que tenemos demasiados selectores, ¿tal vez solo usamos getUser y accedemos a las propiedades de la entidad que necesitamos directamente?
const user = getUser(id)(state); const email = user.email;
Este enfoque resuelve el problema de escribir y admitir una gran cantidad de selectores, pero crea otro problema. Si necesitamos cambiar el modelo de Usuario, también tendremos que controlar todos los lugares donde se user.email
( nota del traductor u otro campo que cambiemos). Con una gran cantidad de código en el proyecto, esto puede convertirse en una tarea difícil y complicar incluso una pequeña refactorización. Cuando teníamos un selector, nos protegía de tales consecuencias de los cambios, porque asumió la responsabilidad de trabajar con el modelo y el código utilizando el selector no sabía nada sobre el modelo.
El acceso directo es comprensible. Pero, ¿qué pasa con la recepción de datos calculados? Por ejemplo, con el nombre de usuario completo, ¿cuál es una concatenación del nombre y apellido? Necesito cavar más lejos ...

El modelo de dominio se dirige. Redux - Secundaria
Puede llegar a esta imagen respondiendo dos preguntas:
- ¿Cómo definimos nuestro modelo de dominio?
- ¿Cómo almacenaremos los datos? (gestión de estado, para ello utilizamos redux * nota del traductor * de que la capa de persistencia se llama en DDD)
Respondiendo a la pregunta "¿Cómo definimos el modelo de dominio" (en nuestro caso, Usuario), resumamos de redux y decidamos qué es un "usuario" y qué API se necesita para interactuar con él?
// 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á bueno si siempre usamos esta API y consideramos que el modelo de Usuario es inaccesible fuera del archivo api.ts. Esto significa que nunca volveremos directamente a los campos de la entidad ya que el código que usa la API ni siquiera sabe qué entidad tiene campos.
Ahora podemos volver a Redux y resolver problemas relacionados solo con el estado:
- ¿Qué lugar ocupan los usuarios en nuestro artículo?
- ¿Cómo debemos almacenar los usuarios? Una lista? Diccionario (clave-valor)? Como mas?
- ¿Cómo obtendremos una instancia de usuario del estado? ¿Debería usarse la memorización? (en el contexto del selector getUser)
API pequeña con grandes beneficios
Aplicando el principio de compartir la responsabilidad entre el área temática y el estado, obtenemos muchos bonos.
Un modelo de dominio bien documentado (modelo de usuario y su API) en el archivo api.ts. Se presta bien a las pruebas, ya que no tiene dependencias Podemos extraer el modelo y la API en la biblioteca para su reutilización en otras aplicaciones.
Podemos combinar fácilmente funciones API como selectores, lo cual es una ventaja incomparable sobre el acceso directo a las propiedades. Además, nuestra interfaz de datos ahora es fácil de mantener en el futuro: podemos cambiar fácilmente el modelo de Usuario sin cambiar el código que lo utiliza.
No ocurrió magia con la API, todavía parece clara. La API se asemeja a lo que se hizo usando selectores, pero tiene una diferencia clave: no necesita todo el estado, ya no necesita soportar el estado completo de la aplicación para la prueba: la API no tiene nada que ver con Redux y su código repetitivo.
Los accesorios del componente se han vuelto más limpios. En lugar de esperar a que se ingresen las propiedades firstName, lastName y email, el componente recibe una instancia de Usuario y usa internamente su API para acceder a los datos necesarios. Resulta que solo necesitamos un selector: getUser.
Hay beneficios para los reductores y el middleware de dicha API. La esencia del beneficio es que primero puede obtener una instancia de Usuario, lidiar con los valores faltantes, procesar o prevenir todos los errores y luego usar los métodos API. Esto es mejor que usar cada campo individual usando selectores aislados del área temática. Por lo tanto, Redux realmente se convierte en un "contenedor predecible" y deja de ser un objeto "divino" con conocimiento de todo.
Conclusión
Con buenas intenciones (lea aquí: selectores), el camino al infierno está pavimentado: no queríamos acceder a los campos de la entidad directamente e hicimos selectores separados para esto.
Aunque la idea de los selectores en sí es buena, su uso excesivo dificulta el mantenimiento de nuestro código.
La solución descrita en el artículo propone resolver el problema en dos etapas: primero describa el modelo de dominio y su API, luego trate con Redux (almacenamiento de datos, selectores). De esta manera, escribirá un código mejor y más pequeño: solo necesita un selector para crear una API más flexible y escalable.
Notas del traductor
- Usé la palabra estado, ya que parece que ha entrado firmemente en el vocabulario de los desarrolladores de habla rusa.
- El autor usa las palabras aguas arriba / aguas abajo para significar "código de alto nivel / bajo nivel" (si está de acuerdo con Martin) o "código que se usa a continuación / código a continuación que usa lo que está escrito arriba", pero no es correcto descubrir cómo usar esto en la traducción Por lo tanto, podría consolarme tratando de no alterar el sentido general.
Con mucho gusto aceptaré comentarios y sugerencias para correcciones en PM y los corregiré.