Cómo React-Redux decide volver a renderizar: selectores, igualdad y ganchos tipados.
Una guía de estudio sobre los aspectos internos de React-Redux: Provider, la igualdad en useSelector, selectores memorizados, connect(), ganchos tipados con withTypes() y casos límite poco comunes.
La superficie de React-Redux parece diminuta: un componente provider y un par de hooks. Sin embargo, en una base de código grande las mismas preguntas siguen apareciendo. ¿Por qué este componente se renderiza con cada dispatch? ¿Por qué un selector que devuelve un objeto se comporta de manera diferente a mapStateToProps? ¿Cuándo es shallowEqual la herramienta adecuada, qué describen realmente RootState y AppDispatch, y ¿se sigue necesitando batch() en React 18?
Esta guía responde a esas preguntas siguiendo un mismo hilo conductor: lo que ocurre entre una acción enviada y un nuevo renderizado. Una vez claro ese proceso, las APIs individuales dejan de ser una lista que hay que memorizar y pasan a formar parte de un sistema sobre el cual se puede razonar, depurar y explicar en una revisión de código o en una entrevista.
useSelector()
useDispatch()
<Provider />
Guía actual: los mantenedores de React-Redux recomiendan la API de Hooks como opción predeterminada para los componentes.
connect()sigue estando soportado y merece ser conocido, ya que mucho código existente depende de él.
Qué es React-Redux y cuál es su papel
Redux gestiona el estado, ejecuta los reducers y notifica a los suscriptores; React se encarga de renderizar la interfaz de usuario. React-Redux es el enlace oficial que permite a los componentes leer desde el almacén de estado y provocar actualizaciones, mientras React sigue a cargo del rendering.
Redux
│
│ Application State
▼
React-Redux
│
│ Integration
▼
React
│
│ UI
▼
User
En concreto, este enlace brinda a los componentes cuatro capacidades:
- leer valores del estado de Redux
- suscribirse a los cambios en esos valores
- enviar acciones al almacén de estado
- vincular las actualizaciones del almacén con el ciclo de rendering de React
Los puntos de entrada modernos para realizar estas tareas son tres hooks:
useSelector()
useDispatch()
useStore()
además del componente que hace que el almacén sea accesible en primer lugar:
<Provider />
Para código más antiguo también existe la API de componentes de orden superior, que sigue teniendo soporte total:
connect()
Redux y React-Redux son paquetes diferentes
Redux en sí mismo es el contenedor de estado y posee estos conceptos:
Store
State
Actions
Reducers
Dispatch
Middleware
Selectors
React-Redux es solo el puente, y todo lo que exporta se refiere a la comunicación con React:
Provider
useSelector
useDispatch
useStore
connect
Apilados juntos, las capas se ven así:
Redux
┌───────────────┐
│ Store │
│ State │
│ Reducers │
│ Actions │
│ Middleware │
└───────┬───────┘
│
▼
React-Redux
┌───────────────┐
│ Provider │
│ useSelector │
│ useDispatch │
│ useStore │
│ connect │
└───────┬───────┘
│
▼
React
Si una pregunta trata sobre cómo cambia el estado (reductores, middleware, estructuras de acciones), pertenece a Redux. Si se refiere a cuándo un componente detecta un cambio o se renderiza, pertenece a React-Redux.
El bucle central que debes tener en mente
Antes de utilizar cualquier API, internalice el ciclo: lea un selector, ejecute una acción ante la interacción, reduzca los datos al nuevo estado, vuelva a ejecutar el selector y muestre el contenido solo si el valor seleccionado ha cambiado.
React Component
│
│ useSelector()
▼
Redux Store
│
│ current state
▼
Component
│
│ user interaction
▼
useDispatch()
│
│ dispatch(action)
▼
Reducer
│
▼
New Redux State
│
▼
useSelector()
│
▼
Re-render if
selected value
changed
Casi todas las preguntas sobre rendimiento relacionadas con React-Redux se refieren al último paso de ese diagrama.
Provider: hacer que el store sea accesible
Los hooks solo pueden comunicarse con un store que esté disponible en algún nivel superior del árbol. <Provider> es lo que lo coloca allí. Por lo general, se envuelve la raíz de la aplicación una sola vez:
import { Provider } from 'react-redux'
import { store } from './store'
function AppRoot() {
return (
<Provider store={store}>
<App />
</Provider>
)
}
En el fondo, Provider coloca el store dentro de un contexto de React. Cualquier descendiente, por más profundo que sea, puede acceder a él sin necesidad de pasar propiedades por niveles intermedios:
<Provider store={store}>
│
├── App
│ ├── Header
│ ├── Dashboard
│ ├── UserProfile
│ └── Settings
│
└── Every descendant
can use Redux
Si un componente llama a uno de los hooks fuera de un Provider, no existe ningún almacén del que leer y el hook falla (en la práctica lanza un error indicando que falta el valor del contexto):
useSelector(...)
useDispatch(...)
Un lugar común donde esto ocurre es en pruebas unitarias que renderizan un componente sin envolverlo en un Provider de prueba.
useSelector: cómo los componentes leen el estado
useSelector() es el hook que utilizarás con mayor frecuencia. Le proporcionas una función que recibe todo el estado de Redux y devuelve la parte que necesita el componente:
const count = useSelector(
state => state.counter.value
)
La estructura del flujo de datos es sencilla:
Redux State
│
▼
useSelector()
│
▼
Selected Value
│
▼
React Component
Dado que React-Redux puede llamar a tu selector con más frecuencia de la esperada (al renderizar, después de los dispatches, durante las verificaciones de desarrollo), el selector debe ser puro: misma entrada, mismo salida, sin efectos secundarios.
Qué hace el hook en cada render y dispatch
Tome un selector que elija al usuario actual:
const user = useSelector(
state => state.auth.user
)
Detrás de esa única línea, React-Redux ejecuta una pequeña rutina:
- lee el estado actual del almacén
- llama a su selector con ese estado
- conserva el valor devuelto como el resultado “últimamente seleccionado”
- registra una suscripción al almacén para este componente
- vuelve a ejecutar el selector después de cada acción enviada
- compara el resultado anterior con el nuevo
- programa una nueva renderización solo cuando la comparación indica que difieren
El paso 6 es de donde proviene casi todo el comportamiento sorprendente.
La comparación por defecto es la igualdad estricta de referencias
Por defecto, el hook
useSelector()
compara los resultados con ===:
previousResult === newResult
Cuando la comparación arroja
true
El selector no da a React-Redux ninguna razón para volver a renderizar el componente. Cuando lo hace
false
se programa la re-renderización del componente.
Esta es una diferencia intencionada con respecto a connect(), que compara de forma superficial el objeto devuelto por mapStateToProps. El código que funcionaba bien con connect() puede comenzar a renderizarse en cada acción una vez se traslada a hooks sin ajustar el selector.
Por qué devolver un objeto nuevo provoca una nueva renderización cada vez
Un primer intento natural para leer dos valores sería este:
const data = useSelector(state => ({
user: state.user,
count: state.count
}))
No hay nada malo con los valores, pero la función flecha crea un nuevo literal de objeto cada vez que se ejecuta:
{
user: ...,
count: ...
}
Dos literales de objeto con contenido idéntico siguen siendo dos objetos diferentes, por lo que la comprobación
oldObject === newObject
siempre da como resultado
false
La consecuencia es una cadena que se activa con cada acción en la aplicación, incluidas aquellas que no tienen nada que ver con los usuarios ni con las cuentas:
Action dispatched
↓
Selector executes
↓
New object created
↓
Different reference
↓
Component re-renders
Este es uno de los errores de rendimiento más frecuentes en React-Redux. La documentación enumera tres soluciones: selectores separados, una función de igualdad personalizada como shallowEqual, o un selector memorizado.
Solución uno: llamar a useSelector una vez por valor
En lugar de agrupar los valores en un objeto,
const data = useSelector(state => ({
user: state.user,
count: state.count
}))
divida la lectura en hooks independientes:
const user = useSelector(
state => state.user
)
const count = useSelector(
state => state.count
)
Cada selector ahora devuelve un valor que ya se encuentra en el almacén, y no un nuevo contenedor alrededor de él. Cuando el estado que los contiene no ha cambiado
user unchanged
count unchanged
las referencias son idénticas y la comprobación con === da como resultado verdadero.
No hay penalización por llamar al hook varias veces en un mismo componente. Si una sola transmisión modifica más de un valor seleccionado, React-Redux agrupa las actualizaciones resultantes, de modo que el componente se renderice solo una vez por esa transmisión y no una vez por cada hook.
Solución dos: shallowEqual para objetos intencionales
A veces, devolver un objeto es realmente la opción más clara, por ejemplo cuando un componente utiliza un conjunto pequeño de campos relacionados. En ese caso, pase shallowEqual como método de comparación:
import {
useSelector,
shallowEqual
} from 'react-redux';
const data = useSelector(
state => ({
user: state.user,
count: state.count
}),
shallowEqual
)
Con él, React-Redux compara cada campo de nivel superior de los objetos antiguos y nuevos utilizando === en lugar de comparar las referencias de los objetos. Un nuevo contenedor con los mismos valores de campo se considera sin cambios.
Las versiones recientes también permiten realizar la comparación a través de un objeto de opciones:
const data = useSelector(
selector,
{
equalityFn: shallowEqual
}
)
Cuándo vale la pena usar shallowEqual
Utilice shallowEqual cuando agrupar valores haga que el componente sea más fácil de leer. No lo trate como algo predeterminado que se aplique automáticamente en cada llamada:
useSelector(selector, shallowEqual)
La mejor pregunta inicial es si el componente podría simplemente seleccionar cada valor por separado. La mayoría de las veces sí puede, y el resultado es más fácil de analizar:
const user = useSelector(
state => state.auth.user
)
const permissions = useSelector(
state => state.auth.permissions
)
Tenga en cuenta que shallowEqual solo examina un nivel de profundidad. Si un campo dentro del objeto devuelto es a su vez un array u objeto recién creado, la comparación sigue fallando.
Seleccionadores que derivan datos
Los selectores hacen más que extraer campos. También son donde se calculan datos derivados, como una lista filtrada:
const selectCompletedTodos = state =>
state.todos.filter(
todo => todo.completed
)
El problema es que
filter()
siempre asigna un nuevo array. Incluso cuando la lista de tareas no ha cambiado de manera significativa,
oldArray !== newArray
Almacena los resultados y el componente se renderiza después de cada acción. La memorización resuelve esto al almacenar en caché la salida basada en las entradas. Cuando las entradas son los mismos objetos que la última vez, se obtiene nuevamente el resultado almacenado en caché:
Same inputs
↓
Return cached result
Cuando una entrada cambia, el cálculo se ejecuta de nuevo:
Changed inputs
↓
Recalculate
createSelector desde Reselect
La herramienta estándar para esto es createSelector, de Reselect (Redux Toolkit lo reexporta). Se enumeran los selectores de entrada y luego una función de resultado que solo se ejecuta cuando esas entradas cambian:
import { createSelector } from 'reselect';
const selectCompletedTodos = createSelector(
state => state.todos,
todos =>
todos.filter(todo => todo.completed)
)
El componente lo utiliza como cualquier otro selector:
const todos = useSelector(
selectCompletedTodos
)
Aunque state.todos sea la misma referencia de array, el selector devuelve su array filtrado anterior, por lo que === da como resultado verdadero. La advertencia: un selector memorizado lleva un caché, por lo que el lugar donde se crea su instancia es importante.
Cree los selectores memorizados que dependen únicamente del estado una sola vez, a nivel de módulo
Cuando un selector memorizado depende solo del estado de Redux,
const selectCompletedTodos = createSelector(
state => state.todos,
todos => ...
)
declárelo fuera de cualquier componente:
const selectCompletedTodos = createSelector(...)
y haga referencia a él desde el componente:
function TodoList() {
const todos = useSelector(
selectCompletedTodos
)
}
Si createSelector se ejecuta dentro del cuerpo del componente, cada renderización creará un nuevo selector con una caché vacía, y la memorización nunca servirá de nada. Una instancia a nivel de módulo persiste entre renderizaciones.
Seleccionadores memorizados que también necesitan propiedades
Los selectores simples, no memorizados, que leen propiedades son inofensivos. Este no mantiene ninguna caché, por lo que no hay nada que pueda salir mal:
function TodoListItem({ id }) {
const todo = useSelector(
state => state.todos[id]
)
return <div>{todo.text}</div>
}
Las cosas se vuelven más complejas cuando un selector memorizado depende de ambos.
Redux State + Component Props
Un selector de este tipo almacena en caché el resultado para sus últimos argumentos. Si muchos elementos de lista comparten una misma instancia, cada uno con un id diferente, siguen invalidando mutuamente su caché. Para un único componente, generalmente basta con crear el selector con useMemo; para muchos componentes, es necesario conocer la estrategia de memorización de la biblioteca (tamaño del caché o una instancia por componente). Este tema surge en entrevistas para puestos de alto nivel y con listas largas.
Los selectores deben mantenerse puros
Un selector debería ser una función simple del estado:
State
↓
Selector
↓
Value
y nunca un lugar donde el código externo afecte al selector:
State
↓
Selector
↓
API call
↓
Mutation
↓
Side effect
El siguiente es el tipo de selector que se debe evitar; los registros, las llamadas a red y las mutaciones no pertenecen aquí:
const selectUser = state => {
console.log('side effect')
// API call ❌
// mutation ❌
return state.user
}
Uso de props dentro de un selector
Un selector definido directamente en un componente puede simplemente acceder a los props del mismo:
function TodoItem({ id }) {
const todo = useSelector(
state => state.todos[id]
)
return <div>{todo.text}</div>
}
El valor pasa de la propiedad al selector a través de las cierres:
id
↓
closure
↓
selector
↓
state.todos[id]
Esto representa una diferencia real con mapStateToProps, que recibe ownProps como segundo argumento. useSelector() no pasa ninguna propiedad, por lo que hay que depender de cierres o de fábricas de selectores que acepten argumentos adicionales.
useDispatch: envío de acciones
Si useSelector() es el lado de lectura,
useSelector
↓
READ
entonces useDispatch() es el lado de escritura:
useDispatch
↓
DISPATCH
Devuelve la función dispatch del almacén, a la que se llama desde los manejadores de eventos:
const dispatch = useDispatch()
function handleClick() {
dispatch(increment())
}
o directamente en JSX:
<button
onClick={() => dispatch(increment())}
>
Increment
</button>
Qué activa un dispatch
Rastrear un clic de principio a fin refuerza el ciclo principal mencionado anteriormente:
User clicks button
↓
dispatch(action)
↓
Redux
↓
Reducer
↓
New state
↓
useSelector()
↓
Component updates
Una llamada concreta que utiliza un creador de acciones y un payload se ve así:
dispatch(
addTodo({
id: 1,
text: 'Learn Redux'
})
)
Callback estables para hijos memorizados
La mayoría de los callback de dispatch no necesitan useCallback. La excepción ocurre cuando un manejador como
const increment = () =>
dispatch(incrementAction())
se pasa a un hijo que está envuelto en React.memo:
<MyButton
onIncrement={increment}
/>
Dado que la función flecha se recrea en cada renderizado del padre, el hijo memorizado recibe una nueva propiedad cada vez y se renderiza de todos modos. Envolver el manejador soluciona este problema:
const increment = useCallback(
() => dispatch(incrementAction()),
[dispatch]
)
junto con un hijo memorizado:
const MyButton = React.memo(...)
Incluir dispatch como dependencia es seguro: su identidad permanece igual siempre y cuando el Provider reciba la misma instancia de store.
useStore: acceso directo, rara vez necesario
El tercer hook te entrega el objeto store en sí:
const store = useStore()
Con él se pueden llamar a los métodos brutos del almacén:
store.getState()
store.dispatch(...)
store.subscribe(...)
Ese acceso casi nunca es lo que un componente debería usar para renderizar datos. La lectura se realiza a través de
useSelector()
y la escritura también se realiza a través de
useDispatch()
La documentación trata useStore() como una solución de último recurso para casos raros, como la inyección de un reducer, y no para lecturas cotidianas.
Por qué store.getState() en render se vuelve obsoleto
Considere un componente que lee directamente del almacén:
function Component() {
const store = useStore()
const user = store.getState().user
return <div>{user.name}</div>
}
Esto se renderiza correctamente una vez y luego queda desactualizado. getState() es una lectura única sin suscripción, por lo que un cambio posterior nunca indica a React que vuelva a renderizar este componente. La versión con suscripción se mantiene sincronizada:
const user = useSelector(
state => state.user
)
Un vistazo a los tres hooks
┌────────────────────────────┐
│ React-Redux Hooks │
├────────────────────────────┤
│ useSelector() │ → READ
│ useDispatch() │ → DISPATCH
│ useStore() │ → STORE ACCESS
└────────────────────────────┘
En el código diario de los componentes, los dos primeros realizan casi todo el trabajo:
90%+
useSelector()
useDispatch()
useStore() aparece solo ocasionalmente.
connect(): la API de componentes de orden superior
Los hooks son el enfoque recomendado, pero connect() sigue estando presente, y muchos conjuntos de código de larga duración se construyen con él. Es probable que encuentres código como este:
connect(
mapStateToProps,
mapDispatchToProps
)(Component)
Cómo connect mapea el almacén a las props
El modelo mental es un envoltorio que convierte los datos del almacén y las funciones de dispatch en props normales:
Redux Store
│
▼
connect()
│
├── mapStateToProps
│
└── mapDispatchToProps
│
▼
Component Props
mapStateToProps recibe el estado y devuelve un objeto de props:
const mapStateToProps = state => ({
user: state.auth.user,
count: state.counter.value
})
En efecto, realiza esta traducción:
Redux State
↓
Component Props
El componente envuelto sigue siendo una función simple de sus props y no sabe que existe Redux:
function User({ user, count }) {
return (
<div>
{user.name}
{count}
</div>
)
}
mapDispatchToProps proporciona las funciones de callback. En su forma funcional, se recibe dispatch y se crean los manejadores por uno mismo:
const mapDispatchToProps =
dispatch => ({
increment: () =>
dispatch(increment())
})
Luego el componente los llama como propiedades:
props.increment()
La forma abreviada de mapDispatchToProps
La forma más concisa pasa un objeto con creadores de acciones:
const mapDispatchToProps = {
increment,
decrement
}
React-Redux enlaza cada creador de acciones para que al llamar a la propiedad se dispare. Esta suele ser la opción más ordenada.
Las cuatro formas comunes de connect
Se encontrarán con cuatro variaciones. Sin argumentos, el componente recibe solo dispatch como propiedad:
connect()(Component)
Con solo un mapeador de estado, el componente lee datos pero no recibe creadores de acciones enlazados:
connect(
mapStateToProps
)(Component)
Al usar null como primer argumento, el componente nunca escucha al store y solo recibe propiedades de dispatch:
connect(
null,
mapDispatchToProps
)(Component)
Y al usar ambos, lee y envía mensajes:
connect(
mapStateToProps,
mapDispatchToProps
)(Component)
Saber que el formulario con null omite la suscripción es útil: es una forma sencilla de darle al componente acceso a dispatch sin que se renderice con las actualizaciones del store.
connect devuelve un nuevo componente
Llamar a
connect(
mapStateToProps,
mapDispatchToProps
)(MyComponent)
nos altera MyComponent. Genera un componente envolvente separado que renderiza el tuyo en su interior:
MyComponent
│
▼
connect()
│
▼
ConnectedComponent
Por eso, los módulos conectados suelen exportar la versión envuelta como predeterminada y, a veces, exportan el componente simple por separado para pruebas.
Elegir entre hooks y connect
Para una aplicación nueva, la respuesta es sencilla:
New React application
↓
Hooks
Para uno ya existente, la respuesta es pragmática:
Existing connect()
↓
Understand and maintain it
Los hooks reducen el código genérico, eliminan los componentes de envoltura y facilitan mucho el uso de TypeScript, por lo que son la opción predeterminada. Los componentes que utilizan connect() no necesitan ser reescritos; conviértalos cuando los modifiques de todos modos.
La diferencia en la igualdad en una sola línea
Tenga presente esta combinación:
useSelector()
↓
=== reference equality
versus
connect()
↓
shallow equality
Muchos errores del tipo “funcionaba antes de la refactorización” provienen de esto: un objeto nuevo era seguro en mapStateToProps, pero falla con === en useSelector().
Tipado de React-Redux con TypeScript
React-Redux incluye sus propias definiciones de tipos, y la documentación describe una configuración tipada estándar. Se basa en seis nombres principales:
RootState
AppDispatch
AppStore
useAppSelector
useAppDispatch
useAppStore
RootState: inferir el tipo de estado desde el almacén
Dado un almacén configurado con Redux Toolkit,
const store = configureStore({
reducer: {
counter: counterReducer,
users: usersReducer
}
})
deriva el tipo de estado a partir de lo que devuelve getState:
export type RootState =
ReturnType<typeof store.getState>
Esto es mejor que escribir la estructura a mano,
type RootState = {
counter: CounterState
users: UsersState
}
porque el tipo inferido sigue automáticamente a los reducers.
AppDispatch: el tipo de dispatch que incluye middleware
Inferir el tipo de dispatch de la misma manera:
export type AppDispatch =
typeof store.dispatch
El tipo simple Dispatch de Redux solo conoce acciones simples; el tipo inferido refleja tu middleware, lo cual es importante una vez que uses:
- middleware que cambia lo que acepta
dispatch - thunks
- un dispatch personalizado
- acciones asíncronas de cualquier tipo
Sin AppDispatch, enviar un thunk genera un error de tipo.
AppStore: el tipo del propio almacén
El tipo del almacén está a solo una inferencia más de distancia:
export type AppStore =
typeof store
De esta manera, cada aspecto del sistema tiene una única fuente de verdad. El tipo de estado:
RootState
↓
state type
El tipo de envío:
AppDispatch
↓
dispatch type
El tipo de almacén:
AppStore
↓
store type
AppStore resulta especialmente útil cuando se crea un almacén por solicitud o por prueba y es necesario pasarlo entre diferentes partes del código.
Ganchos pre-tipados con withTypes()
A partir de React-Redux 9.1.0, cada gancho expone un método .withTypes():
useDispatch.withTypes()
useSelector.withTypes()
useStore.withTypes()
El patrón documentado permite crear ganchos específicos para la aplicación a partir de ellos una sola vez:
export const useAppDispatch =
useDispatch.withTypes<AppDispatch>()
export const useAppSelector =
useSelector.withTypes<RootState>()
export const useAppStore =
useStore.withTypes<AppStore>()
Qué ahorran los ganchos tipados
Sin ellos, cada selector necesita una anotación explícita:
const user = useSelector(
(state: RootState) =>
state.auth.user
)
Con el gancho tipado,
const user = useAppSelector(
state => state.auth.user
)
el compilador ya lo sabe
state = RootState
El proceso de envío funciona de la misma manera:
const dispatch = useAppDispatch()
Este dispatch acepta thunks y cualquier otra cosa que permita su middleware, con verificación completa.
Un archivo hooks.ts para la aplicación
Un proyecto típico guarda estos elementos en un módulo pequeño:
import {
useDispatch,
useSelector,
useStore
} from 'react-redux'
import type {
RootState,
AppDispatch,
AppStore
} from './store'
export const useAppDispatch =
useDispatch.withTypes<AppDispatch>()
export const useAppSelector =
useSelector.withTypes<RootState>()
export const useAppStore =
useStore.withTypes<AppStore>()
Los componentes importan desde ese módulo en lugar de hacerlo directamente desde react-redux:
const user = useAppSelector(
state => state.auth.user
)
const dispatch = useAppDispatch()
ConnectedProps para código connect() tipado
Los conjuntos de código que tipan los componentes connect() contendrán
ConnectedProps
Divida la llamada a connect en un conector primero, y luego extraiga las propiedades que este inyecta:
const connector = connect(
mapState,
mapDispatch
)
type PropsFromRedux =
ConnectedProps<typeof connector>
PropsFromRedux describe con exactitud qué inyecta el conector, por lo que los tipos mapeados nunca se duplican.
Diseño de buenos selectores
Es tentador pensar en un selector como algo más que
state => state.user
En una aplicación más grande, los selectores deben considerarse como un límite que transforma el formato de almacenamiento del estado en la forma que desea la interfaz de usuario:
Redux State
↓
Selector
↓
UI-friendly data
Por ejemplo, la regla que determina cuáles elementos están visibles puede encontrarse en una función con nombre específico:
const selectVisibleTodos =
state =>
state.todos.filter(
todo => !todo.hidden
)
y el componente simplemente solicita el resultado:
const todos = useSelector(
selectVisibleTodos
)
El componente se mantiene orientado a la presentación, y la regla puede probarse por separado.
Un buen selector es preciso
const selectUserName =
state => state.auth.user.name
Devuelve exactamente lo que muestra el componente y nada más, por lo que solo activa un renderizado cuando ese nombre cambia.
Elegir todo el estado es casi siempre un error
const selectEverything =
state => state
Redux genera un nuevo objeto de estado raíz cada vez que algún reductor modifica algo. Por lo tanto, un selector que devuelve el estado raíz proporciona una nueva referencia prácticamente después de cada acción:
Anything in Redux changes
↓
Root state reference changes
↓
Selector result changes
↓
Component re-renders
Las comprobaciones de desarrollo de React-Redux marcan este patrón.
Mantenga los selectores específicos
Prefiera varias lecturas dirigidas:
const count =
useSelector(
state => state.counter.value
)
const user =
useSelector(
state => state.auth.currentUser
)
en lugar de una sola lectura general:
const state =
useSelector(state => state)
Una regla práctica:
Elija la parte más pequeña del estado que el componente pueda utilizar realmente.
Comprobaciones de selectores en modo desarrollo
Las versiones recientes de React-Redux realizan comprobaciones adicionales en sus selectores durante las compilaciones de desarrollo. Dos de ellas merecen ser conocidas por su nombre.
La comprobación de estabilidad
La primera comprobación llama a su selector una segunda vez con el mismo estado y compara los resultados:
selector(state)
↓
run again with same state
↓
same result?
Si el resultado es
same reference
el selector es estable. Si no lo es
new reference
React-Redux emite una advertencia porque un selector que devuelve una nueva referencia para datos idénticos volverá a renderizar su componente en cada actualización del almacén.
Un ejemplo típico es el selector de tipo literal de objeto mencionado anteriormente:
const data = useSelector(
state => ({
count: state.count,
user: state.user
})
)
Dado que el objeto se reconstruye en cada llamada, la verificación detecta:
same input
↓
different object
↓
unstable selector
Configuración de la frecuencia con la que se ejecutan las verificaciones
Puedes establecer la frecuencia para toda la aplicación en el Provider:
<Provider
store={store}
stabilityCheck="always"
>
<App />
</Provider>
o sobrescribirla para una llamada específica de hook:
const count = useSelector(
selectCount,
{
devModeChecks: {
stabilityCheck: 'once'
}
}
)
Los valores aceptados son:
never
once
always
El valor predeterminado es 'once', lo que significa que la verificación se ejecuta en la primera llamada de cada hook. Nada de esto se ejecuta en las versiones para producción.
Verificación de la función de identidad
La segunda verificación busca un selector que devuelva sus datos sin cambios:
state => state
En un componente, se ve así:
const state = useSelector(
state => state
)
Esto vincula el componente a cada cambio en el almacén:
Any Redux change
↓
Root state changes
↓
Component re-renders
La documentación denomina esto la verificación de función de identidad. En versiones anteriores se conocía como noopCheck, nombre que aún puede aparecer en configuraciones antiguas.
La solución es la misma que antes: reemplazar
const state = useSelector(
state => state
)
por lecturas de los valores específicos que necesitas:
const count = useSelector(
state => state.counter.value
)
const user = useSelector(
state => state.auth.currentUser
)
Renderizado y rendimiento más allá de los selectores
El renderizado del padre sigue teniendo efecto en cascada
useSelector() solo controla los renderizados causados por actualizaciones del almacén. No hace nada respecto a la regla normal de React según la cual un componente se renderiza cuando su padre se renderiza:
Parent renders
↓
Child renders
Eso ocurre incluso si no ha habido ningún cambio en el estado de Redux. Cuando un hijo es costoso y sus props son estables, envuélvelo en
React.memo()
Esto difiere de connect(), cuyo wrapper se comporta como un componente memorizado; los componentes basados en hooks no cuentan con este tipo de comportamiento de forma gratuita.
Combinar React.memo con useSelector
Aquí el componente se suscribe a un contador y también se memoriza en función de su propiedad name:
const Counter = ({ name }) => {
const count = useSelector(
state => state.counter.value
)
return (
<div>
{name}: {count}
</div>
)
}
export default React.memo(Counter)
Estos dos mecanismos abarcan las dos fuentes de renderizado:
Redux selector
+
React.memo
↓
More controlled rendering
La memorización tiene su propio costo, por lo que primero realice un análisis del rendimiento. Para obtener un catálogo más amplio de los factores que provocan el renderizado en React, consulte nuestra guía sobre patrones comunes que causan renderizaciones repetidas innecesarias en React.
Un modelo de rendimiento que cabe en una sola pantalla
Después de cada acción, React-Redux vuelve a ejecutar los selectores de los componentes suscritos, compara cada resultado con el anterior y renderiza únicamente aquellos componentes cuyos resultados difieren:
Redux action
↓
Store updates
↓
Selectors execute
↓
Selector results compared
↓
Changed?
┌───┴────┐
No Yes
│ │
│ ▼
│ Re-render
│
└── No Redux-triggered render
Por lo tanto, su tarea es escribir selectores que:
- devuelvan solo los datos que utiliza el componente
- proporcionen referencias estables cuando no ha cambiado nada relevante
- nunca asignen nuevos objetos o arrays sin necesidad
- memoicen los datos derivados cuyo cálculo es costoso
Regla uno: nunca seleccione el estado raíz
useSelector(state => state)
Regla dos: evite crear objetos de envoltura en los selectores
Un selector como este asigna recursos en cada ejecución:
useSelector(state => ({
user: state.user,
count: state.count
}))
Devuelva un objeto así solo cuando lo combine deliberadamente con
shallowEqual
o lo pase a través de un selector memoizado.
Regla tres: memoicen las derivaciones costosas
El ordenamiento, filtrado, agrupación y unión pertenecen a
createSelector(...)
Regla cuatro: aplica React.memo sobre los elementos relevantes
Antes de memorizar un componente, revisa una breve lista de verificación:
Is the component expensive?
↓
Does it receive stable props?
↓
Does it re-render unnecessarily?
↓
Then consider React.memo()
Si tu base de código utiliza el React Compiler, gran parte de esta memorización manual ya podría estar gestionada.
Regla cinco: elige los elementos de la forma más específica que permita el componente
Muy amplio:
state => state
Más adecuado:
state => state.auth.user
Aún mejor, cuando el componente muestra únicamente el nombre:
state => state.auth.user.name
Casos límite raros: props obsoletos e hijos inactivos
La mayoría de las aplicaciones nunca se enfrentan a estos dos problemas, pero explican por qué los selectores defensivos son una buena práctica.
Props obsoletos
Para resolver el problema de los props obsoletos se necesita un selector que dependa de un prop, además de una actualización del almacén que modifique tanto el estado como, indirectamente, ese prop:
Selector depends on component props
↓
Redux action updates state
↓
Parent would receive new props
↓
Child selector runs first
↓
Selector sees old props
La suscripción del hijo puede activarse antes de que el padre haya vuelto a renderizar y transmitido la nueva propiedad. Por un momento, el selector combina el estado actual con una propiedad antigua. Una forma típica es:
const todo = useSelector(
state => state.todos[props.id]
)
Si el elemento con ese id acaba de ser eliminado, o si el padre está a punto de transmitir un id diferente, el selector lee brevemente datos que ya no coinciden.
Creación de selectores que toleren datos faltantes
La versión frágil asume que el elemento siempre existe:
state.todos[props.id].name
Una versión defensiva primero busca el elemento,
const todo =
state.todos[props.id]
y solo entonces lee de él:
return todo
? todo.name
: undefined
La decisión se toma mediante una simple comprobación:
Does data exist?
↓
Yes → use it
No → handle safely
La cadena opcional (state.todos[id]?.name) expresa la misma idea en una sola expresión.
Hijos zombis
El escenario del hijo zombi implica un padre que muestra una lista y un hijo que se suscribe a un elemento específico:
Parent
│
└── Child
La secuencia es la siguiente:
- El hijo se suscribe al almacén de datos.
- Una acción elimina los datos que muestra el hijo.
- El padre dejará de mostrar a ese hijo en su próxima actualización.
- Antes de que eso ocurra, se ejecuta la suscripción del hijo.
- El selector del hijo intenta acceder a datos que ya no existen.
Un selector sin protección genera un error en ese último paso. React-Redux cuenta con mecanismos para los errores de selector causados por actualizaciones del almacén, pero los selectores defensivos siguen siendo la solución más sólida.
Por qué los hooks son más vulnerables que connect
connect() crea un árbol de suscripciones anidado: cada componente conectado solo se actualiza después de que lo hayan hecho sus ancestros conectados, lo cual impone un orden de arriba hacia abajo. Los hooks se adjuntan directamente al store sin esa jerarquía, por lo que las garantías de orden son más débiles y estos casos límite se vuelven teóricamente posibles.
Considérelo como conocimiento de fondo, no como una razón para evitar los hooks. La postura documentada es que estos problemas son poco comunes en las aplicaciones reales.
Funcionalidades avanzadas de Provider
Un contexto personalizado para stores aislados
Por defecto,
<Provider store={store}>
publica el store a través del contexto integrado de React-Redux. Una biblioteca de componentes reutilizables que utiliza Redux internamente puede chocar de esa manera con el store de la aplicación anfitriona. Para evitarlo, Provider acepta un contexto propio:
<Provider
context={MyContext}
store={myStore}
>
Los ganchos vinculados a ese contexto provienen de funciones de fábrica:
createStoreHook()
createDispatchHook()
createSelectorHook()
Esto es importante principalmente para bibliotecas reutilizables donde, de lo contrario, varios almacenes colisionarían.
batch() y el agrupamiento automático de React 18
El código más antiguo suele envolver envíos consecutivos en batch() para que React se renderice una sola vez en lugar de dos:
batch(() => {
dispatch(action1())
dispatch(action2())
})
React 18 agrupa automáticamente las actualizaciones, incluidas las de promesas y tiempos de espera, por lo que una aplicación típica de React 18 no necesita batch() para esto. Las versiones recientes de React-Redux lo mantienen principalmente por compatibilidad; consulte la documentación actual y espere encontrarlo en código más antiguo.
serverState para SSR e hidratación
Para el renderizado del lado servidor, Provider recibe un prop adicional:
<Provider
store={store}
serverState={preloadedState}
>
El servidor renderiza HTML a partir de un estado inicial y envía ese estado al navegador. serverState hace que la renderización de hidratación utilice el mismo estado, evitando discrepancias:
Server
↓
Initial Redux State
↓
HTML
↓
Browser Hydration
↓
Provider(serverState)
↓
Consistent initial render
Una estructura típica de proyecto
Una aplicación en TypeScript construida con Redux Toolkit suele organizarse por funcionalidades, con la configuración del almacén en un único lugar:
src/
│
├── app/
│ ├── store.ts
│ └── hooks.ts
│
├── features/
│ │
│ ├── counter/
│ │ ├── counterSlice.ts
│ │ └── Counter.tsx
│ │
│ ├── users/
│ │ ├── usersSlice.ts
│ │ └── Users.tsx
│ │
│ └── auth/
│ ├── authSlice.ts
│ └── Login.tsx
│
├── App.tsx
└── main.tsx
store.ts
El módulo del almacén configura los reducers y exporta los tipos inferidos:
const store = configureStore({
reducer: {
counter: counterReducer,
users: usersReducer,
auth: authReducer
}
})
export type RootState =
ReturnType<typeof store.getState>
export type AppDispatch =
typeof store.dispatch
export type AppStore =
typeof store
hooks.ts
El módulo de hooks convierte esos tipos en hooks de la aplicación:
export const useAppDispatch =
useDispatch.withTypes<AppDispatch>()
export const useAppSelector =
useSelector.withTypes<RootState>()
export const useAppStore =
useStore.withTypes<AppStore>()
Un componente que utiliza ambos
Luego, un componente de contador lee y escribe únicamente a través de los hooks tipados:
function Counter() {
const count = useAppSelector(
state => state.counter.value
)
const dispatch = useAppDispatch()
return (
<>
<span>{count}</span>
<button
onClick={() =>
dispatch(increment())
}
>
+
</button>
</>
)
}
El flujo dentro de este componente es exactamente el bucle principal desde el inicio:
Component
│
├── useAppSelector()
│ ↓
│ READ
│
└── useAppDispatch()
↓
DISPATCH
↓
Redux
↓
New State
↓
useAppSelector()
↓
Component
Dónde encaja Redux Toolkit
Redux Toolkit y React-Redux son complementarios, no alternativas:
Redux Toolkit
+
React-Redux
Redux Toolkit mejora el aspecto relacionado con Redux: la configuración del almacén, los reducers, la lógica asíncrona, los selectores memorizados y la obtención de datos.
configureStore
createSlice
createAsyncThunk
createSelector
RTK Query
React-Redux sigue siendo responsable de la conexión con React:
Provider
useSelector
useDispatch
useStore
connect
La guía rápida oficial de React-Redux configura ambos juntos, y esa combinación es la predeterminada para nuevos proyectos.
El flujo completo en un único diagrama
Reuniendo todas las partes, desde el Provider hasta la comprobación de igualdad:
React
│
▼
<Provider>
│
▼
Redux Store
│
┌────────┴────────┐
│ │
useSelector() useDispatch()
│ │
│ ▼
│ Action
│ │
│ ▼
│ Reducer
│ │
│ ▼
│ New State
│ │
└─────────┬───────┘
▼
Selector runs
│
▼
Equality check
│
┌──────┴──────┐
│ │
Same Different
│ │
▼ ▼
No Redux Re-render
render
Errores comunes y cómo detectarlos
Suscribirse al estado completo
useSelector(state => state)
Sustitúyalo por selectores específicos.
Rodear valores en un nuevo objeto
useSelector(state => ({
user: state.user
}))
Divídelo en hooks separados, o agrega intencionadamente shallowEqual.
Filtrado o mapeo dentro del selector en cada ejecución
useSelector(state =>
state.todos.filter(...)
)
Si esto se ejecuta con frecuencia o en listas grandes, muévalo a un selector memorizado.
Lectura de datos de renderización mediante useStore
Llamar a
store.getState()
dentro de la lógica de renderización le proporciona un valor sin suscripción. Utilice
useSelector()
para que el componente se actualice cuando cambie el valor.
Mutación directa del estado
Una asignación como
state.user.name = 'John'
rompe el modelo de actualización inmutable de Redux: las referencias no cambian, por lo que los selectores detectan “ningún cambio” y los componentes no se renderizan. (Dentro de los reducers createSlice de Redux Toolkit, este estilo está permitido porque Immer lo convierte en una actualización inmutable; en cualquier otro lugar es un error.)
Efectos secundarios dentro de los selectores
state => {
fetch(...)
return state.user
}
Un selector debe calcular y devolver, nada más.
Memorización por reflejo
Dispersar estos elementos por todas partes
useMemo()
useCallback()
React.memo()
añade complejidad y sobrecarga en las comparaciones sin pruebas de que sea útil. Optimice el proceso de renderizado que ya ha medido.
Colocación incorrecta de instancias de selectores memorizados
Un selector con caché se comporta de manera diferente según su ámbito. Antes de usarlo, averigüe si es:
global
per component
per component instance
Una instancia compartida utilizada con muchos argumentos diferentes puede no acceder nunca a su caché.
Preguntas de entrevista con respuestas cortas
¿Qué es React-Redux y por qué se necesita Provider?
Es el enlace oficial de React con Redux: Provider; los hooks y connect permiten a los componentes leer el estado, reaccionar a los cambios y enviar acciones. Provider coloca la tienda de datos en el contexto de React para que cualquier componente descendiente pueda acceder a ella.
useSelector versus useDispatch?
Uno sirve para leer:
useSelector
↓
READ Redux state
Lo que escribe el otro:
useDispatch
↓
DISPATCH Redux actions
¿Cómo hace useSelector que se vuelva a renderizar?
Después de cada acción, vuelve a ejecutar el selector y compara el resultado con el anterior utilizando === o la función de igualdad que se pasó. Solo una diferencia provoca que se ejecute el renderizado.
¿Por qué este selector hace que se vuelva a renderizar constantemente, y cómo se soluciona?
useSelector(state => ({
user: state.user,
count: state.count
}))
Se crea un nuevo objeto en cada ejecución, por lo que la verificación de referencia siempre falla. Las tres soluciones:
1. Multiple useSelector calls
2. shallowEqual
3. Memoized selector
useSelector frente a connect?
Los hooks comparan los resultados del selector por referencia; connect() compara de forma superficial las propiedades de mapStateToProps. Los hooks son la opción por defecto, mientras que connect() sigue estando disponible.
¿Por qué usar selectores memorizados?
Vuelven a calcular los datos derivados solo cuando cambian las entradas y, de lo contrario, devuelven la misma referencia. Un filtro es el caso clásico:
todos
↓
filter completed
↓
new array
¿Qué son RootState, AppDispatch y withTypes()?
El tipo inferido de todo el estado:
type RootState =
ReturnType<typeof store.getState>
El tipo de dispatch inferido, incluyendo middleware como thunks:
type AppDispatch =
typeof store.dispatch
Y las herramientas que devuelven ganchos previnculados a esos tipos:
useDispatch.withTypes<AppDispatch>()
useSelector.withTypes<RootState>()
useStore.withTypes<AppStore>()
¿Por qué ganchos tipados?
Sustituyen anotaciones repetidas como
useSelector(
(state: RootState) =>
state.user
)
por
useAppSelector(
state => state.user
)
¿Por qué los selectores deben ser puros?
Un selector puede ejecutarse varias veces para el mismo estado, durante la renderización, después de cada acción y en momentos que tu código no controla. Cualquier efecto secundario se ejecutaría un número impredecible de veces.
¿Para qué sirve useStore?
Casos poco comunes que realmente necesitan el objeto de almacenamiento. La lectura del estado para la renderización se realiza a través de useSelector().
¿Qué son los hijos zombi y las propiedades obsoletas?
Ambos son problemas de ordenación raros. Un hijo zombi procesa una actualización antes de que su padre lo desmonte y lee datos eliminados; las propiedades obsoletas significan que un selector dependiente de propiedades se ejecuta con estado actualizado pero usando propiedades antiguas. Los selectores defensivos manejan ambos casos.
¿Sigue siendo necesario batch() en React 18?
Por lo general no, ya que React 18 procesa los cambios en lotes de forma automática, pero aún se puede encontrar en código más antiguo.
¿Por qué connect y los hooks pueden renderizar de manera diferente?
Modelos de suscripción diferentes y comparaciones distintas:
connect()
↓
shallow equality
versus
useSelector()
↓
strict === equality
¿Por qué se ejecuta useSelector con tanta frecuencia?
Esto ocurre porque:
- se ejecuta durante la renderización
- escucha los cambios en el almacenamiento
Por lo tanto, un selector inline, al ser una función nueva en cada renderización, se ejecuta en cada una; una referencia de selector estable permite a React-Redux omitir esa llamada.
Qué estudiar, por orden de prioridad
Nivel uno: debe conocerse al dedillo
Provider
useSelector
useDispatch
Redux Store flow
Selectors
=== equality
Re-render behavior
Redux Toolkit + React-Redux
TypeScript
RootState
AppDispatch
.withTypes()
Nivel dos: conocimiento práctico sólido
shallowEqual
Memoized selectors
createSelector
useStore
connect
mapStateToProps
mapDispatchToProps
ConnectedProps
React.memo
Nivel tres: temas avanzados
Stale props
Zombie children
Selector + props
Selector memoization
Custom context
Development mode checks
SSR serverState
batch()
Qué no memorizar
No es necesario aprender la documentación línea por línea. Está bien saltarse:
- cómo se implementa internamente el mecanismo de suscripción
- opciones de
connect()poco utilizadas - patrones obsoletos desde hace tiempo
- detalles de implementación del código fuente
Lo importante es el razonamiento detrás de las reglas. Conocer ese hecho
useSelector uses ===
es menos útil que poder responder
Why?
a saber:
Because returning a new object
creates a new reference.
lo cual conduce a la cadena
New reference
↓
=== false
↓
re-render
Si comprendes esa cadena, podrás deducir la mayoría de las demás reglas por ti mismo.
Diez reglas a seguir
1. Leer con useSelector
useSelector()
es la forma en que los componentes leen el estado.
2. Escribir con useDispatch
useDispatch()
es la forma en que los componentes envían acciones.
3. Envolver la aplicación en Provider
<Provider store={store}>
4. Recordar las reglas de comparación
useSelector → ===
connect → shallow comparison
5. Nunca seleccionar el estado raíz
useSelector(state => state)
6. Tener cuidado con los selectores que devuelven objetos
useSelector(state => ({
...
}))
7. Memorizar datos derivados costosos
Memoized selector
8. Mantener los selectores organizados
Pure
Predictable
Granular
9. Escribir primero el store y luego los hooks
Primero los tipos inferidos:
RootState
AppDispatch
AppStore
luego los hooks de la aplicación creados a partir de ellos:
useAppSelector
useAppDispatch
useAppStore
10. Utilizar la combinación moderna
Redux Toolkit
+
React-Redux Hooks
Toda la API en una sola página
Una referencia compacta que abarca los hooks principales, las buenas prácticas de rendimiento, los tipos de TypeScript, la API heredada y los temas avanzados:
┌───────────────────────────────────────────────┐
│ REACT-REDUX │
├───────────────────────────────────────────────┤
│ │
│ <Provider store={store}> │
│ ↓ │
│ Makes Redux available to React │
│ │
│ useSelector() │
│ ↓ │
│ READ state │
│ ↓ │
│ Default comparison: === │
│ │
│ useDispatch() │
│ ↓ │
│ DISPATCH actions │
│ │
│ useStore() │
│ ↓ │
│ Direct store access │
│ ↓ │
│ Use rarely │
│ │
├───────────────────────────────────────────────┤
│ PERFORMANCE │
├───────────────────────────────────────────────┤
│ │
│ Avoid state => state │
│ Avoid unnecessary object creation │
│ Use granular selectors │
│ Use shallowEqual when appropriate │
│ Use memoized selectors for derived data │
│ Use React.memo when justified │
│ │
├───────────────────────────────────────────────┤
│ TYPESCRIPT │
├───────────────────────────────────────────────┤
│ │
│ RootState = ReturnType<typeof store.getState> │
│ AppDispatch = typeof store.dispatch │
│ AppStore = typeof store │
│ │
│ useAppSelector │
│ useAppDispatch │
│ useAppStore │
│ │
├───────────────────────────────────────────────┤
│ LEGACY / EXISTING APPLICATIONS │
├───────────────────────────────────────────────┤
│ │
│ connect() │
│ mapStateToProps │
│ mapDispatchToProps │
│ ConnectedProps │
│ │
├───────────────────────────────────────────────┤
│ ADVANCED │
├───────────────────────────────────────────────┤
│ │
│ Stale Props │
│ Zombie Children │
│ Custom Context │
│ SSR serverState │
│ Development checks │
│ batch() │
│ │
└───────────────────────────────────────────────┘
Y el ciclo, dibujado una vez más con las rutas de lectura y escritura uno al lado del otro:
REACT
│
│
<Provider />
│
▼
┌─────────────┐
│ Redux Store │
└──────┬──────┘
│
┌───────────┴───────────┐
│ │
▼ ▲
useSelector() useDispatch()
│ │
│ │
READ ACTION
│ │
│ │
│ ┌────┴─────┐
│ │ Reducer │
│ └────┬─────┘
│ │
│ ▼
│ New State
│ │
└───────────────────────┘
│
▼
Equality Check
│
┌──────┴──────┐
│ │
Same Changed
│ │
▼ ▼
No Redux Re-render
render
Para un nuevo proyecto en TypeScript, la configuración recomendada se reduce a esta estructura:
Redux Toolkit
+
React-Redux
│
┌──────────┴──────────┐
│ │
Store Provider
│ │
│ Application tree
│ │
└──────────┬──────────┘
│
┌────────┴────────┐
│ │
useAppSelector() useAppDispatch()
│ │
READ WRITE
│ │
└────────┬────────┘
│
Redux
│
New State
│
Selector
│
Re-render
Conclusión
React-Redux se vuelve mucho más sencillo una vez que dejas de tratar sus exportaciones como herramientas independientes. Esta lista de nombres
Provider
useSelector
useDispatch
connect
shallowEqual
createSelector
useStore
Describe un único pipeline con una tarea por etapa. El proveedor expone el almacén:
Provider
↓
makes Store available
El gancho selector lee:
useSelector
↓
reads selected state
El gancho dispatch envía:
useDispatch
↓
sends actions
Los reducers generan el siguiente estado:
Reducer
↓
creates new state
Los selectores lo dan forma para la interfaz de usuario:
Selector
↓
derives data
La comprobación de igualdad decide si ha cambiado algo relevante:
Equality
↓
decides whether selected data changed
Y React se encarga de la renderización:
React
↓
re-renders when necessary
En el lado de TypeScript, la cadena también es lineal:
Store
↓
RootState
AppDispatch
AppStore
↓
.withTypes()
↓
useAppSelector()
useAppDispatch()
useAppStore()
Cuando un componente renderiza más de lo necesario, hay que analizar las mismas cuatro preguntas cada vez:
useSelector()
↓
What does my selector return?
↓
Is the reference stable?
↓
Does the selected value actually change?
↓
Should this component re-render?
Puntos clave:
- La mayoría de los problemas de rendimiento en React-Redux provienen de selectores que devuelven nuevas referencias, no del propio Redux.
useSelector() compara con ===; connect() realiza una comparación superficial. Portar código de uno a otro sin ajustar los selectores modifica el comportamiento.createSelector a nivel de módulo, y utilice shallowEqual solo cuando se desee obtener un resultado de tipo objeto.RootState, AppDispatch y AppStore a partir de la tienda de estado y cree ganchos tipados con .withTypes().batch() y serverState merecen ser comprendidos, pero se trata de casos extremos y no de problemas cotidianos.Como autoprueba, intente explicar de memoria cada sección de esta guía, desde Provider e igualdad pasando por los hooks tipados hasta serverState, y escriba la configuración básica sin consultar nada.
Para más detalles, las referencias oficiales son la API de hooks, la API de Provider, la guía de uso con TypeScript, el Inicio rápido y la referencia de connect(). Estude primero el bucle principal y la igualdad, luego los selectores, TypeScript, el rendimiento, connect y los casos límite, para que los detalles de la API tengan un modelo al cual aferrarse.
Lecturas relacionadas
- React Query y Redux: Repensando el estado del servidor en aplicaciones grandes — Aprenda por qué una aplicación de chat en producción utilizó TanStack Query en lugar de Redux para gestionar los datos del servidor, y dónde sigue teniendo cabida Redux en la arquitectura moderna de React.
- Estructurando una capa de datos de TanStack Query, desde queryOptions hasta Rollbacks — Construya una capa de datos de TanStack Query paso a paso: queryOptions compartidos, generadores de claves, selectores, paginación, precarga, invalidación centralizada y actualizaciones óptimas seguras.