Derivación de datos con selectores
Esta página fue traducida por PageTurner AI (beta). No está respaldada oficialmente por el proyecto. ¿Encontraste un error? Reportar problema →
- Por qué una buena arquitectura de Redux mantiene el estado mínimo y deriva datos adicionales
- Principios de uso de funciones selectoras para derivar datos y encapsular búsquedas
- Cómo usar la biblioteca Reselect para escribir selectores memoizados y optimizados
- Técnicas avanzadas para usar Reselect
- Herramientas y bibliotecas adicionales para crear selectores
- Mejores prácticas para escribir selectores
Derivación de datos
Recomendamos específicamente que las aplicaciones Redux mantengan el estado de Redux mínimo y deriven valores adicionales de ese estado siempre que sea posible.
Esto incluye operaciones como calcular listas filtradas o sumar valores. Por ejemplo, una app de tareas mantendría una lista original de objetos todo en el estado, pero derivaría una lista filtrada de tareas fuera del estado cada vez que se actualice. De igual forma, verificar si todas las tareas están completadas o contar las tareas pendientes también puede calcularse fuera del store.
Esto tiene varios beneficios:
-
El estado real es más fácil de leer
-
Se necesita menos lógica para calcular esos valores adicionales y mantenerlos sincronizados
-
El estado original permanece como referencia sin ser reemplazado
¡Este es también un buen principio para el estado de React! Muchas veces los usuarios intentan definir un hook useEffect que espera cambios en un valor de estado y luego actualiza el estado con un valor derivado como setAllCompleted(allCompleted). En su lugar, ese valor puede derivarse durante el renderizado y usarse directamente, sin necesidad de guardarlo en el estado:
function TodoList() {
const [todos, setTodos] = useState([])
// Derive the data while rendering
const allTodosCompleted = todos.every(todo => todo.completed)
// render with this value
}
Cálculo de datos derivados con selectores
En una aplicación Redux típica, la lógica para derivar datos suele escribirse como funciones que llamamos selectores.
Los selectores se usan principalmente para:
- Encapsular lógica de búsqueda de valores específicos en el estado
- Contener lógica para derivar valores reales
- Mejorar rendimiento evitando recálculos innecesarios
No estás obligado a usar selectores para todas las consultas de estado, pero son un patrón estándar ampliamente adoptado.
Conceptos básicos de selectores
Una "función selector" es cualquier función que acepta el estado completo de Redux (o una parte) como argumento y devuelve datos basados en ese estado.
Los selectores no requieren bibliotecas especiales y da igual si usas funciones flecha o la palabra clave function. Por ejemplo, todas estas son funciones selector válidas:
// Arrow function, direct lookup
const selectEntities = state => state.entities
// Function declaration, mapping over an array to derive values
function selectItemIds(state) {
return state.items.map(item => item.id)
}
// Function declaration, encapsulating a deep lookup
function selectSomeSpecificField(state) {
return state.some.deeply.nested.field
}
// Arrow function, deriving values from an array
const selectItemsWhoseNamesStartWith = (items, namePrefix) =>
items.filter(item => item.name.startsWith(namePrefix))
Puedes nombrar las funciones selector como prefieras. Sin embargo, recomendamos prefijar sus nombres con select combinado con una descripción del valor seleccionado. Ejemplos típicos serían selectTodoById, selectFilteredTodos y selectVisibleTodos.
Si has usado el hook useSelector de React-Redux, ya conoces la idea básica: las funciones que pasamos a useSelector deben ser selectores:
function TodoList() {
// This anonymous arrow function is a selector!
const todos = useSelector(state => state.todos)
}
Las funciones selector suelen definirse en dos lugares de una aplicación Redux:
-
En ficheros de slice (rebanadas), junto a la lógica del reducer
-
En ficheros de componentes, fuera del componente o inline en llamadas a
useSelector
Una función selector puede utilizarse en cualquier lugar donde tengas acceso al estado raíz completo de Redux. Esto incluye el hook useSelector, la función mapState para connect, middleware, thunks y sagas. Por ejemplo, los thunks y el middleware tienen acceso al argumento getState, por lo que puedes invocar un selector allí:
function addTodosIfAllowed(todoText) {
return (dispatch, getState) => {
const state = getState()
const canAddTodos = selectCanAddTodos(state)
if (canAddTodos) {
dispatch(todoAdded(todoText))
}
}
}
Normalmente no es posible usar selectores dentro de reducers, porque un slice reducer solo accede a su propia porción del estado de Redux, y la mayoría de selectores esperan recibir el estado raíz completo como argumento.
Encapsulación de la forma del estado con selectores
La primera razón para usar funciones selector es la encapsulación y reutilización al manejar la estructura de tu estado de Redux.
Supongamos que uno de tus hooks useSelector realiza una búsqueda muy específica en parte de tu estado de Redux:
const data = useSelector(state => state.some.deeply.nested.field)
Este código es válido y funcionará, pero podría no ser la mejor opción arquitectónica. Imagina que tienes varios componentes que necesitan acceder a ese campo. ¿Qué ocurre si necesitas cambiar la ubicación de ese dato? Tendrías que modificar cada hook useSelector que lo referencia. Al igual que recomendamos usar creadores de acciones para encapsular detalles de creación de acciones, aconsejamos definir selectores reutilizables para encapsular la ubicación de cada dato. Así podrás usar la misma función selector múltiples veces donde sea necesario.
Idealmente, solo tus funciones reducer y selectores deberían conocer la estructura exacta del estado. Si cambias la ubicación de un dato, solo necesitarías actualizar estas dos piezas de lógica.
Por ello, suele ser buena idea definir selectores reutilizables directamente en archivos de slices, en lugar de siempre dentro de componentes.
Una descripción común es que los selectores son como "consultas a tu estado". No te importa cómo obtiene los datos, solo que al solicitarlos recibes un resultado.
Optimización de selectores con memoización
Las funciones selector a menudo realizan cálculos "costosos" o crean valores derivados con nuevas referencias de objetos/arrays. Esto afecta al rendimiento por:
-
Selectores usados con
useSelectoromapStatese re-ejecutan tras cada acción, aunque no cambie su parte relevante del estado. Recalcular innecesariamente desperdicia recursos. -
useSelectorymapStateusan igualdad referencial (===) para determinar re-renderizados. Si un selector siempre devuelve nuevas referencias, forzará re-renderizados innecesarios. Esto es común con operaciones comomap()/filter()que devuelven nuevos arrays.
Este componente está mal implementado porque su useSelector siempre devuelve una nueva referencia de array, causando re-render tras cada acción aunque state.todos no cambie:
function TodoList() {
// ❌ WARNING: this _always_ returns a new reference, so it will _always_ re-render!
const completedTodos = useSelector(state =>
state.todos.filter(todo => todo.completed)
)
}
Otro ejemplo es un componente que necesita realizar un trabajo "costoso" para transformar los datos:
function ExampleComplexComponent() {
const data = useSelector(state => {
const initialData = state.data
const filteredData = expensiveFiltering(initialData)
const sortedData = expensiveSorting(filteredData)
const transformedData = expensiveTransformation(sortedData)
return transformedData
})
}
De manera similar, esta lógica "costosa" se volverá a ejecutar después de cada acción despachada. No solo probablemente creará nuevas referencias, sino que es un trabajo que no necesita hacerse a menos que state.data cambie realmente.
Necesitamos una forma de escribir selectores optimizados que puedan evitar recálculos si se pasan las mismas entradas. Ahí es donde entra la idea de la memoización.
La memoización es una forma de caché. Consiste en rastrear las entradas de una función y almacenar tanto estas como los resultados para su uso posterior. Si una función recibe las mismas entradas que antes, puede omitir el trabajo real y devolver el mismo resultado que generó la última vez que recibió esos valores de entrada. Esto optimiza el rendimiento al realizar trabajo solo cuando las entradas cambian, devolviendo consistentemente las mismas referencias de resultado si las entradas son idénticas.
A continuación, veremos algunas opciones para escribir selectores memoizados.
Escribiendo selectores memoizados con Reselect
Tradicionalmente, el ecosistema de Redux ha utilizado una biblioteca llamada Reselect para crear funciones selectoras memoizadas. También existen otras bibliotecas similares, así como múltiples variantes y wrappers alrededor de Reselect; las veremos más adelante.
Visión general de createSelector
Reselect proporciona una función llamada createSelector para generar selectores memoizados. createSelector acepta una o más funciones "selectoras de entrada", más una función "selectora de salida", y devuelve una nueva función selectora para usar.
createSelector está incluido como parte de nuestro paquete oficial Redux Toolkit y se reexporta para facilitar su uso.
createSelector puede aceptar múltiples selectores de entrada, que pueden proporcionarse como argumentos separados o en un array. Los resultados de todos los selectores de entrada se proporcionan como argumentos separados al selector de salida:
const selectA = state => state.a
const selectB = state => state.b
const selectC = state => state.c
const selectABC = createSelector([selectA, selectB, selectC], (a, b, c) => {
// do something with a, b, and c, and return a result
return a + b + c
})
// Call the selector function and get a result
const abc = selectABC(state)
// could also be written as separate arguments, and works exactly the same
const selectABC2 = createSelector(selectA, selectB, selectC, (a, b, c) => {
// do something with a, b, and c, and return a result
return a + b + c
})
Cuando llamas al selector, Reselect ejecutará tus selectores de entrada con todos los argumentos proporcionados y examinará los valores devueltos. Si alguno de los resultados es === diferente a antes, volverá a ejecutar el selector de salida y pasará esos resultados como argumentos. Si todos los resultados son iguales a la última vez, omitirá volver a ejecutar el selector de salida y simplemente devolverá el resultado final en caché anterior.
Esto significa que los "selectores de entrada" normalmente deberían solo extraer y devolver valores, mientras que el "selector de salida" debe realizar el trabajo de transformación.
Un error bastante común es escribir un "selector de entrada" que extrae un valor o realiza alguna derivación, y un "selector de salida" que simplemente devuelve su resultado:
// ❌ BROKEN: this will not memoize correctly, and does nothing useful!
const brokenSelector = createSelector(
state => state.todos,
todos => todos
)
¡Cualquier "selector de salida" que solo devuelva sus entradas es incorrecto! El selector de salida siempre debe contener la lógica de transformación.
Del mismo modo, ¡un selector memoizado nunca debe usar state => state como entrada! Eso forzaría al selector a recalcular siempre.
En el uso típico de Reselect, escribes tus "selectores de entrada" de nivel superior como funciones simples que solo devuelven valores anidados dentro del objeto de estado. Luego, usas createSelector para crear selectores memoizados que toman uno o más de estos valores como entrada y producen nuevos valores derivados:
const selectTodos = state => state.todos.items
const selectCurrentUser = state => state.users.currentUser
const selectTodosForCurrentUser = createSelector(
[selectTodos, selectCurrentUser],
(todos, currentUser) => {
console.log('Output selector running')
return todos.filter(todo => todo.ownerId === currentUser.userId)
}
)
const todosForCurrentUser1 = selectTodosForCurrentUser(state)
// Log: "Output selector running"
const todosForCurrentUser2 = selectTodosForCurrentUser(state)
// No log output
console.log(todosForCurrentUser1 === todosForCurrentUser2)
// true
Observa que la segunda vez que llamamos a selectTodosForCurrentUser, el "selector de salida" no se ejecutó. Debido a que los resultados de selectTodos y selectCurrentUser fueron iguales que en la primera llamada, selectTodosForCurrentUser pudo devolver el resultado memoizado de la primera llamada.
Comportamiento de createSelector
Es importante señalar que, por defecto, createSelector solo memoiza el conjunto más reciente de parámetros. Esto significa que si llamas repetidamente a un selector con diferentes entradas, aún devolverá un resultado, pero tendrá que volver a ejecutar el selector de salida para producirlo:
const a = someSelector(state, 1) // first call, not memoized
const b = someSelector(state, 1) // same inputs, memoized
const c = someSelector(state, 2) // different inputs, not memoized
const d = someSelector(state, 1) // different inputs from last time, not memoized
Además, puedes pasar múltiples argumentos a un selector. Reselect llamará a todos los selectores de entrada con esas entradas exactas:
const selectItems = state => state.items
const selectItemId = (state, itemId) => itemId
const selectItemById = createSelector(
[selectItems, selectItemId],
(items, itemId) => items[itemId]
)
const item = selectItemById(state, 42)
/*
Internally, Reselect does something like this:
const firstArg = selectItems(state, 42);
const secondArg = selectItemId(state, 42);
const result = outputSelector(firstArg, secondArg);
return result;
*/
Por este motivo, es importante que todos los "selectores de entrada" que proporciones acepten los mismos tipos de parámetros. De lo contrario, los selectores fallarán.
const selectItems = state => state.items
// expects a number as the second argument
const selectItemId = (state, itemId) => itemId
// expects an object as the second argument
const selectOtherField = (state, someObject) => someObject.someField
const selectItemById = createSelector(
[selectItems, selectItemId, selectOtherField],
(items, itemId, someField) => items[itemId]
)
En este ejemplo, selectItemId espera que su segundo argumento sea un valor simple, mientras que selectOtherField espera que el segundo argumento sea un objeto. Si llamas a selectItemById(state, 42), selectOtherField fallará porque intenta acceder a 42.someField.
Patrones de uso y limitaciones de Reselect
Anidar selectores
Es posible tomar selectores generados con createSelector y usarlos como entrada para otros selectores. En este ejemplo, el selector selectCompletedTodos se usa como entrada para selectCompletedTodoDescriptions:
const selectTodos = state => state.todos
const selectCompletedTodos = createSelector([selectTodos], todos =>
todos.filter(todo => todo.completed)
)
const selectCompletedTodoDescriptions = createSelector(
[selectCompletedTodos],
completedTodos => completedTodos.map(todo => todo.text)
)
Pasar parámetros de entrada
Una función selector generada por Reselect puede llamarse con tantos argumentos como quieras: selectThings(a, b, c, d, e). Sin embargo, lo que importa para volver a ejecutar la salida no es el número de argumentos ni si los argumentos mismos han cambiado a nuevas referencias. En cambio, depende de los "selectores de entrada" definidos y si sus resultados han cambiado. Del mismo modo, los argumentos para el "selector de salida" se basan únicamente en lo que devuelven los selectores de entrada.
Esto significa que si quieres pasar parámetros adicionales al selector de salida, debes definir selectores de entrada que extraigan esos valores de los argumentos originales del selector:
const selectItemsByCategory = createSelector(
[
// Usual first input - extract value from `state`
state => state.items,
// Take the second arg, `category`, and forward to the output selector
(state, category) => category
],
// Output selector gets (`items, category)` as args
(items, category) => items.filter(item => item.category === category)
)
Luego puedes usar el selector así:
const electronicItems = selectItemsByCategory(state, "electronics");
Para mantener la consistencia, puedes considerar pasar parámetros adicionales a un selector como un único objeto, como selectThings(state, otherArgs), y luego extraer valores del objeto otherArgs.
Fábricas de selectores
createSelector tiene un tamaño de caché predeterminado de solo 1, y esto es por cada instancia única de un selector. Esto crea problemas cuando una única función selector necesita reutilizarse en múltiples lugares con entradas diferentes.
Una opción es crear una "fábrica de selectores" - una función que ejecuta createSelector() y genera una nueva instancia única de selector cada vez que se llama:
const makeSelectItemsByCategory = () => {
const selectItemsByCategory = createSelector(
[state => state.items, (state, category) => category],
(items, category) => items.filter(item => item.category === category)
)
return selectItemsByCategory
}
Esto es particularmente útil cuando múltiples componentes UI similares necesitan derivar diferentes subconjuntos de datos basados en props.
Bibliotecas alternativas de selectores
Aunque Reselect es la biblioteca de selectores más utilizada con Redux, existen muchas otras bibliotecas que resuelven problemas similares o amplían las capacidades de Reselect.
proxy-memoize
proxy-memoize es una biblioteca de selectores memoizados relativamente nueva que utiliza un enfoque de implementación único. Se basa en objetos Proxy de ES2015 para rastrear intentos de lectura de valores anidados, luego compara solo los valores anidados en llamadas posteriores para ver si han cambiado. Esto puede proporcionar mejores resultados que Reselect en algunos casos.
Un buen ejemplo de esto es un selector que deriva un array de descripciones de tareas:
import { createSelector } from 'reselect'
const selectTodoDescriptionsReselect = createSelector(
[state => state.todos],
todos => todos.map(todo => todo.text)
)
Desafortunadamente, esto volverá a calcular el array derivado si cualquier otro valor dentro de state.todos cambia, como alternar una bandera todo.completed. Los contenidos del array derivado son idénticos, pero como el array de entrada todos cambió, debe calcular un nuevo array de salida que tiene una nueva referencia.
El mismo selector con proxy-memoize podría verse así:
import { memoize } from 'proxy-memoize'
const selectTodoDescriptionsProxy = memoize(state =>
state.todos.map(todo => todo.text)
)
A diferencia de Reselect, proxy-memoize puede detectar que solo se accede a los campos todo.text, y solo volverá a calcular el resultado si uno de los campos todo.text cambia.
También tiene una opción size incorporada que te permite establecer el tamaño de caché deseado para una única instancia del selector.
Tiene algunas compensaciones y diferencias respecto a Reselect:
-
Todos los valores se pasan como un único argumento de objeto
-
Requiere que el entorno admita objetos
Proxyde ES2015 (sin IE11) -
Es más "mágico", mientras que Reselect es más explícito
-
Hay algunos casos extremos con respecto al comportamiento de seguimiento basado en
Proxy -
Es más reciente y menos utilizado
Dicho esto, oficialmente recomendamos considerar proxy-memoize como una alternativa viable a Reselect.
re-reselect
https://github.com/toomuchdesign/re-reselect mejora el comportamiento de caché de Reselect al permitir definir un "selector de clave". Esto gestiona múltiples instancias internas de selectores Reselect, simplificando su uso en varios componentes.
import { createCachedSelector } from 're-reselect'
const getUsersByLibrary = createCachedSelector(
// inputSelectors
getUsers,
getLibraryId,
// resultFunc
(users, libraryId) => expensiveComputation(users, libraryId)
)(
// re-reselect keySelector (receives selectors' arguments)
// Use "libraryName" as cacheKey
(_state_, libraryName) => libraryName
)
reselect-tools
A veces es difícil rastrear cómo se relacionan varios selectores Reselect o qué causó su recálculo. https://github.com/skortchmark9/reselect-tools proporciona herramientas para trazar dependencias y visualizar relaciones entre selectores.
redux-views
https://github.com/josepot/redux-views es similar a re-reselect al seleccionar claves únicas para un almacenamiento en caché consistente. Diseñado como reemplazo casi directo de Reselect, fue propuesto para una posible versión 5.
Propuesta para Reselect v5
Hemos abierto una discusión sobre la hoja de ruta en el repositorio de Reselect para posibles mejoras en futuras versiones, como optimizar cachés grandes o reescribir en TypeScript. Agradecemos feedback:
Discusión sobre la hoja de ruta de Reselect v5: Objetivos y diseño de la API
Uso de selectores con React-Redux
Llamar a selectores con parámetros
Es común querer pasar argumentos adicionales a una función de selector. Sin embargo, useSelector siempre llama a la función selector proporcionada con un único argumento: el state raíz de Redux.
La solución más simple es pasar un selector anónimo a useSelector, y luego llamar inmediatamente al selector real con ambos state y cualquier argumento adicional:
import { selectTodoById } from './todosSlice'
function TodoListitem({ todoId }) {
// Captures `todoId` from scope, gets `state` as an arg, and forwards both
// to the actual selector function to extract the result
const todo = useSelector(state => selectTodoById(state, todoId))
}
Crear instancias únicas de selectores
Cuando un selector se reutiliza en múltiples componentes con argumentos diferentes, se rompe la memorización (nunca recibe los mismos argumentos consecutivos).
El enfoque estándar aquí es crear una instancia única de un selector memorizado en el componente, y luego usarlo con useSelector. Esto permite que cada componente pase consistentemente los mismos argumentos a su propia instancia de selector, y que ese selector pueda memorizar correctamente los resultados.
En componentes funcionales, se usa useMemo o useCallback:
import { makeSelectItemsByCategory } from './categoriesSlice'
function CategoryList({ category }) {
// Create a new memoized selector, for each component instance, on mount
const selectItemsByCategory = useMemo(makeSelectItemsByCategory, [])
const itemsByCategory = useSelector(state =>
selectItemsByCategory(state, category)
)
}
En componentes de clase con connect, esto se puede lograr mediante una sintaxis avanzada de "función factory" para mapState. Si la función mapState retorna una nueva función en su primera llamada, esta se usará como la función mapState real. Esto proporciona un cierre donde puedes crear una nueva instancia del selector:
import { makeSelectItemsByCategory } from './categoriesSlice'
const makeMapState = (state, ownProps) => {
// Closure - create a new unique selector instance here,
// and this will run once for every component instance
const selectItemsByCategory = makeSelectItemsByCategory()
const realMapState = (state, ownProps) => {
return {
itemsByCategory: selectItemsByCategory(state, ownProps.category)
}
}
// Returning a function here will tell `connect` to use it as
// `mapState` instead of the original one given to `connect`
return realMapState
}
export default connect(makeMapState)(CategoryList)
Uso efectivo de selectores
Aunque los selectores son un patrón común en Redux, a menudo se usan mal. Algunas pautas:
Definir selectores junto a reducers
Los selectores suelen definirse en la capa UI dentro de useSelector, causando repetición entre archivos y funciones anónimas.
Como cualquier función, puedes extraerla fuera del componente para nombrarla:
const selectTodos = state => state.todos
function TodoList() {
const todos = useSelector(selectTodos)
}
Pero múltiples partes de la app pueden necesitar las mismas consultas. Además, conceptualmente conviene mantener el conocimiento de la estructura del estado todos como detalle de implementación en todosSlice.
Por ello, es una buena idea definir selectores reutilizables junto a sus correspondientes reductores. En este caso, podríamos exportar selectTodos desde el archivo todosSlice:
import { createSlice } from '@reduxjs/toolkit'
const todosSlice = createSlice({
name: 'todos',
initialState: [],
reducers: {
todoAdded(state, action) {
state.push(action.payload)
}
}
})
export const { todoAdded } = todosSlice.actions
export default todosSlice.reducer
// Export a reusable selector here
export const selectTodos = state => state.todos
De esta manera, si actualizamos la estructura del estado del segmento de tareas, los selectores relevantes están aquí y pueden actualizarse simultáneamente, con cambios mínimos en otras partes de la aplicación.
Equilibrio en el uso de selectores
Es posible añadir demasiados selectores a una aplicación. ¡Crear funciones de selector separadas para cada campo individual no es buena idea! Esto convierte Redux en algo similar a una clase Java con funciones getter/setter para cada campo. No mejorará el código y probablemente lo empeorará: mantener todos esos selectores extra requiere mucho esfuerzo adicional y será más difícil rastrear qué valores se usan dónde.
Del mismo modo, ¡no hagas que cada selector sea memoizado!. La memoización solo es necesaria si el selector devuelve una nueva referencia cada vez que se ejecuta, o si la lógica de cálculo que realiza es costosa. Una función selector que realiza una búsqueda directa y devuelve un valor debe ser una función simple, no memoizada.
Algunos ejemplos de cuándo memoizar y cuándo no:
// ❌ DO NOT memoize: will always return a consistent reference
const selectTodos = state => state.todos
const selectNestedValue = state => state.some.deeply.nested.field
const selectTodoById = (state, todoId) => state.todos[todoId]
// 🤔 MAYBE memoize: deriving data, but will return a consistent result.
// Memoization might be useful if the selector is used in many places
// or the list being iterated over is long.
const selectItemsTotal = state => {
return state.items.reduce((result, item) => {
return result + item.total
}, 0)
}
const selectAllCompleted = state => state.todos.every(todo => todo.completed)
// ✅ SHOULD memoize: returns new references when called
const selectTodoDescriptions = state => state.todos.map(todo => todo.text)
Reformular el estado según necesidades de componentes
Los selectores no deben limitarse a búsquedas directas: pueden realizar cualquier lógica de transformación necesaria. Esto es especialmente valioso para preparar datos que necesitan componentes específicos.
Un estado de Redux suele contener datos en forma "bruta" porque el estado debe mantenerse mínimo, y muchos componentes pueden necesitar presentar los mismos datos de forma diferente. Puedes usar selectores no solo para extraer el estado, sino para reformularlo según las necesidades de cada componente. Esto puede incluir extraer datos de múltiples segmentos del estado raíz, obtener valores específicos, fusionar diferentes piezas de datos o cualquier otra transformación útil.
Está bien que un componente incluya parte de esta lógica, pero puede ser beneficioso extraer toda esta lógica de transformación en selectores separados para mejor reutilización y testabilidad.
Globalizar selectores si es necesario
Existe un desequilibrio inherente entre escribir reductores de segmentos y selectores. Los reductores de segmentos solo conocen su porción del estado: para el reductor, su state es todo lo que existe (como el array de tareas en todoSlice). Los selectores, sin embargo, normalmente se escriben para recibir todo el estado raíz de Redux como argumento. Esto significa que deben saber dónde están los datos de este segmento en el estado raíz (como state.todos), aunque esto no se define realmente hasta crear el reductor raíz (normalmente en la lógica de configuración global de la store).
Un archivo de segmento típico suele tener ambos patrones. Esto es válido, especialmente en apps pequeñas o medianas. Pero dependiendo de tu arquitectura, quizá quieras abstraer más los selectores para que no sepan dónde está el estado del segmento: debe proporcionárseles.
Llamamos a este patrón "globalizar" selectores. Un selector "globalizado" acepta el estado raíz de Redux como argumento y sabe encontrar el segmento relevante para aplicar la lógica. Un selector "localizado" espera solo una parte del estado como argumento, sin importarle dónde esté en el estado raíz:
// "Globalized" - accepts root state, knows to find data at `state.todos`
const selectAllTodosCompletedGlobalized = state =>
state.todos.every(todo => todo.completed)
// "Localized" - only accepts `todos` as argument, doesn't know where that came from
const selectAllTodosCompletedLocalized = todos =>
todos.every(todo => todo.completed)
Los selectores "localizados" pueden convertirse en "globalizados" envolviéndolos en una función que sepa recuperar el segmento correcto y pasarlo adelante.
La API createEntityAdapter de Redux Toolkit es un ejemplo de este patrón. Si llamas a todosAdapter.getSelectors() sin argumentos, devuelve un conjunto de selectores "localizados" que esperan el estado del slice de entidad como argumento. Si llamas a todosAdapter.getSelectors(state => state.todos), devuelve selectores "globalizados" que esperan recibir el estado raíz de Redux como argumento.
También puede haber otras ventajas al tener versiones "localizadas" de los selectores. Por ejemplo, imagina un escenario avanzado donde mantenemos múltiples copias de datos de createEntityAdapter anidadas en el store, como un chatRoomsAdapter que rastrea salas, y cada definición de sala tiene un estado chatMessagesAdapter para almacenar mensajes. No podemos buscar directamente los mensajes de cada sala: primero debemos recuperar el objeto de la sala y luego seleccionar los mensajes. Esto es más fácil si tenemos un conjunto de selectores "localizados" para los mensajes.
Más información
-
Bibliotecas de selectores:
- Reselect: https://github.com/reduxjs/reselect
proxy-memoize: https://github.com/dai-shi/proxy-memoizere-reselect: https://github.com/toomuchdesign/re-reselectreselect-tools: https://github.com/skortchmark9/reselect-toolsredux-views: https://github.com/josepot/redux-views
-
Debate sobre la hoja de ruta de Reselect v5: Objetivos y diseño de API
Randy Coulman tiene una excelente serie de entradas de blog sobre arquitectura de selectores y diferentes enfoques para globalizar selectores de Redux, con sus compensaciones: