reselect y createSelector: memoizar la derivación
Un selector derivado que recomputa en cada lectura y devuelve una referencia nueva paga dos precios: recalcular lo caro y disparar renders inútiles. reselect resuelve ambos con memoización: mismas entradas, mismo resultado cacheado por identidad. Esta lección diseca createSelector —selectores de entrada más función de resultado—, el cambio de 2026 al memoizador weakMapMemoize por defecto, el problema clásico de la caché de tamaño uno y el patrón factory que lo resolvía, cómo elegir y afinar el memoizador con lruMemoize y createSelectorCreator, los chequeos de estabilidad en modo desarrollo y createDraftSafeSelector para leer dentro de reducers.
La lección anterior terminó en una grieta: un selector derivado que filtra o proyecta crea una referencia nueva en cada llamada, y esa referencia nueva engaña a useSelector, que cree que el dato cambió y re-renderiza sin motivo. A eso se suma el coste bruto de recalcular una derivación cara —ordenar, agrupar, agregar— cada vez que cualquier parte del store se toca. reselect, la librería de memoización que Redux Toolkit trae incluida, ataca los dos problemas con una sola idea: si las entradas de un cálculo no han cambiado, no lo recalcules y devuelve el mismo resultado de antes, con su misma referencia. Memoizar no es solo ir más rápido; es convertir el concepto vago de igualdad en la moneda exacta que React usa para decidir qué trabajo puede saltarse.
- Explicar los dos costes que memoizar elimina: recomputación cara e inestabilidad de referencia.
- Diseccionar la anatomía de
createSelector: selectores de entrada y función de resultado. - Entender el cambio de 2026 a
weakMapMemoizecomo memoizador por defecto y qué problema borra. - Elegir y afinar el memoizador con
lruMemoize,createSelectorCreatory los chequeos de desarrollo.
Por qué memoizar: los dos costes
Conviene separar los dos males que la memoización cura, porque se confunden. El primero es de rendimiento: una derivación cara —ordenar mil filas, agrupar, calcular totales— se ejecuta en cada render aunque sus datos de entrada no hayan cambiado. El segundo es de identidad: aunque el cálculo fuera barato, si devuelve un objeto o array nuevo, esa referencia distinta hace que useSelector, React.memo y cualquier comparación por === crean que algo cambió. Memoizar resuelve ambos de un golpe: cuando las entradas son las mismas, no se recalcula (cura el rendimiento) y se devuelve el resultado anterior con su referencia intacta (cura la identidad).
El segundo coste es el más insidioso porque es invisible en pantalla: nada se ve mal, pero cada despacho —venga de donde venga, aunque no toque tus datos— repinta componentes que no cambiaron. En una lista larga o un dashboard, esa cascada silenciosa de renders es la causa número uno de una interfaz que se siente lenta sin que ningún cálculo concreto parezca culpable.
createSelector: entradas y función de resultado
createSelector recibe una lista de selectores de entrada y una función de resultado. Ejecuta primero los selectores de entrada; solo si alguno devuelve una referencia distinta a la de la llamada anterior vuelve a ejecutar la función de resultado. Si todas las entradas son idénticas, la función de resultado ni se llama: devuelve el valor cacheado.
import { createSelector } from '@reduxjs/toolkit' // reselect va incluido
const selectTodos = (s: RootState) => s.todos.entidades
const selectFiltro = (s: RootState) => s.todos.filtro
// Entradas + funcion de resultado. La funcion de resultado solo se
// re-ejecuta si selectTodos o selectFiltro cambian de REFERENCIA.
export const selectVisibles = createSelector(
[selectTodos, selectFiltro],
(todos, filtro) => Object.values(todos).filter((t) => t.estado === filtro),
)
// Una entrada puede leer argumentos ademas del estado (props del componente).
export const selectPorPrioridad = createSelector(
[selectTodos, (_s: RootState, min: number) => min],
(todos, min) => Object.values(todos).filter((t) => t.prioridad >= min),
)
flowchart TD A[cambio en el store] --> B[ejecutar selectores de entrada] B --> C[alguna entrada cambio de referencia?] C -->|no| D[devolver resultado memoizado] C -->|si| E[ejecutar funcion de resultado] E --> F[guardar nuevo resultado en cache] style D fill:#a6e3a1,color:#11111b style E fill:#fab387,color:#11111b
De aquí se deduce la regla de oro de diseñar buenos selectores memoizados: las entradas deben ser lo más estrechas y estables posible. Si un selector de entrada devuelve una rama que apenas cambia, la función de resultado casi nunca corre; si devuelve todo el estado, o peor, un objeto construido al vuelo, la caché no acierta jamás y la memoización no sirve de nada.
La memoización por defecto en 2026: weakMapMemoize
Durante años, reselect memoizaba con una caché de tamaño uno: recordaba solo la última combinación de entradas. Bastaba llamar al mismo selector alternando dos argumentos para que la caché se destruyera en cada llamada —el temido cache thrashing—. Desde reselect 5, incluido en Redux Toolkit 2.0, el memoizador por defecto es weakMapMemoize: cachea por identidad de las entradas usando WeakMap anidados, con una caché prácticamente ilimitada y recolectada sola por el recolector de basura. En la práctica, el mismo selector llamado con muchos argumentos distintos mantiene un resultado memoizado para cada combinación, sin que tú gestiones nada.
El detalle de que use WeakMap no es un tecnicismo: significa que las claves de la caché son referencias débiles, así que cuando un objeto de entrada deja de existir en el resto del programa, su entrada en la caché se libera sola. No hay fuga de memoria ni límite que ajustar; la caché crece y mengua siguiendo la vida de los datos que memoiza. Esa es la razón por la que pudo convertirse en el defecto sin obligar a nadie a preocuparse por su tamaño.
El superpoder de weakMapMemoize tiene una letra pequeña: las entradas se comparan por identidad, no por igualdad de valor. Si pasas argumentos primitivos —un id string, un número— funciona de maravilla. Pero si un selector de entrada devuelve un objeto o array nuevo en cada llamada, ninguna clave de la WeakMap coincide nunca y vuelves a recomputar siempre, memoizador nuevo o viejo. La memoización no perdona un selector de entrada inestable: primero haz que las entradas sean referencias estables, y solo entonces la caché trabajará para ti.
En 2026, Redux Toolkit corre en modo desarrollo dos chequeos que este tema hace imprescindibles. El inputStabilityCheck ejecuta los selectores de entrada dos veces con el mismo estado y avisa si devuelven referencias distintas —justo el error que mata la memoización—. El identityFunctionCheck avisa si tu función de resultado es una identidad (x => x), señal de que ese createSelector no memoiza nada y sobra. Ambos son gratis y convierten un bug silencioso de rendimiento en un warning explícito en consola.
El problema clásico, el factory y afinar el memoizador
Con la vieja caché de tamaño uno, un selector parametrizado compartido —selectTodoPorId(state, id) usado por muchos componentes con id distintos a la vez— se autodestruía en cada render. La solución idiomática era la selector factory: una función que fabrica una instancia nueva del selector por componente, memoizada con useMemo, de modo que cada uno tuviera su propia caché.
import { createSelector, lruMemoize } from '@reduxjs/toolkit'
// Patron factory: una instancia de selector por consumidor.
const makeSelectTotalPorCategoria = () =>
createSelector(
[(s: RootState) => s.items, (_s: RootState, cat: string) => cat],
(items, cat) => items.filter((i) => i.cat === cat).length,
)
// Afinar: volver al memoizador clasico de tamano uno, con LRU explicito.
const selectCaro = createSelector(
[selectA, selectB],
(a, b) => calcular(a, b),
{ memoize: lruMemoize, memoizeOptions: { maxSize: 10 } },
)
En 2026, weakMapMemoize hace innecesario el factory en la mayoría de los casos: la caché por identidad ya distingue cada id sin que tú fabriques instancias. Pero el patrón sigue siendo parte de la cultura del ecosistema, aparece en código que mantendrás y sigue siendo la respuesta cuando necesitas controlar el ciclo de vida de la caché a mano. Para afinar más allá del defecto, createSelector acepta un tercer argumento con memoize, argsMemoize y memoizeOptions, y createSelectorCreator te deja fabricar tu propia variante —por ejemplo, una que compare las entradas por igualdad profunda cuando de verdad no puedas estabilizar sus referencias—.
Un caso límite que conviene conocer: si necesitas correr un selector memoizado dentro de un reducer de createSlice, el estado que recibes es un draft de Immer, no el objeto final, y la memoización normal puede leer una versión obsoleta. createDraftSafeSelector construye un selector que detecta el draft y se salta la caché mientras dura la mutación, devolviendo resultados correctos tanto sobre el estado real como sobre el borrador. Es la pieza que te deja reutilizar la misma lógica de lectura en el read y en el write sin sorpresas.
El error es pensar que reselect sirve para ir más rápido. La velocidad es un efecto secundario; lo esencial es que la memoización traduce una noción semántica y cara —dos resultados son iguales— a la noción sintáctica y barata que las máquinas de reactividad hablan: dos referencias son idénticas. React, useSelector, React.memo, el algoritmo de reconciliación entero razonan comparando referencias con ===, porque comparar referencias es una operación de coste constante y comparar contenidos no lo es. Un selector sin memoizar rompe ese lenguaje: produce resultados iguales con referencias distintas, y obliga al sistema a recalcular y re-renderizar sobre una mentira. Un selector memoizado restaura el pacto: mientras las entradas no cambien de identidad, la salida conserva la suya, y toda la maquinaria de arriba puede confiar en que ‘misma referencia’ significa ‘mismos datos’ y saltarse el trabajo con total seguridad. Por eso memoizar bien no es una optimización que se añade al final, sino la condición que hace que la reactividad basada en referencias —la de React, la de los signals, la de casi todo el ecosistema— sea correcta y no solo veloz. Y por eso el arte no está en envolver todo en createSelector, sino en diseñar entradas estables: la memoización solo es tan buena como la estabilidad de aquello que compara, y un selector con entradas inestables no es más que recomputación cara con una capa de ceremonia encima.
- Convierte el selector derivado inestable de la lección anterior en un
createSelectorcon entradas estrechas y comprueba que los renders inútiles desaparecen. - Provoca cache thrashing a propósito con un selector de entrada que devuelva
{ ...algo }en cada llamada, y observa que nunca acierta en caché. - Repara ese selector estabilizando la entrada —pasa un primitivo o una rama estable— y verifica que ahora sí memoiza; confirma que el
inputStabilityCheckdeja de avisar. - Fuerza
lruMemoizeconmaxSizepequeño y describe en qué escenario esa caché acotada supera aweakMapMemoize. - Escribe una factory con
useMemopara un selector parametrizado y razona si en 2026 aún la necesitas o si el defecto ya la vuelve superflua. - Crea con
createSelectorCreatorun memoizador que compare por igualdad superficial y explica qué problema concreto resuelve que el defecto no.