Inicio / Artículos / Cómo React-Redux decide volver a renderizar: selectores, igualdad y ganchos tipados.

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.

7738 palabras

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:

  1. lee el estado actual del almacén
  2. llama a su selector con ese estado
  3. conserva el valor devuelto como el resultado “últimamente seleccionado”
  4. registra una suscripción al almacén para este componente
  5. vuelve a ejecutar el selector después de cada acción enviada
  6. compara el resultado anterior con el nuevo
  7. 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:

  1. devuelvan solo los datos que utiliza el componente
  2. proporcionen referencias estables cuando no ha cambiado nada relevante
  3. nunca asignen nuevos objetos o arrays sin necesidad
  4. 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:

  1. El hijo se suscribe al almacén de datos.
  2. Una acción elimina los datos que muestra el hijo.
  3. El padre dejará de mostrar a ese hijo en su próxima actualización.
  4. Antes de que eso ocurra, se ejecuta la suscripción del hijo.
  5. 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:

  1. se ejecuta durante la renderización
  2. escucha los cambios en el almacenamiento
  • se vuelve a ejecutar después de cada acción enviada
  • reutiliza su resultado en caché durante la renderización solo cuando la función selector y el estado no han cambiado
  • 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
  • el texto exacto de cada advertencia de desarrollo
  • cada prop avanzado de Provider
  • 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.
  • Elija selectores de forma específica, memorice los datos derivados con instancias de createSelector a nivel de módulo, y utilice shallowEqual solo cuando se desee obtener un resultado de tipo objeto.
  • Infiera RootState, AppDispatch y AppStore a partir de la tienda de estado y cree ganchos tipados con .withTypes().
  • Los props obsoletos, los hijos “zombi”, los contextos personalizados, 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

  • Prop Drilling no es una razón para instalar Redux o Zustand — Prueba cuatro motivos comunes para agregar una biblioteca de estado en código React funcional: el Contexto para el prop drilling, useState, useSyncExternalStore y el costo de volver a renderizar del Contexto.
  • El scheduler de React y el bucle de eventos: ¿quién decide realmente cuándo se ejecuta el trabajo? — Descubre cómo funciona el scheduler cooperativo de React dentro del bucle de eventos de JavaScript, por qué las transiciones pueden ceder el control y por qué ningún scheduler puede rescatar un hilo bloqueado.
  • Razonamiento sobre React Hooks a través de capturas de renderizado y compromisos — Un modelo mental de nivel avanzado para los Hooks de React y React Native: capturas de renderizado, efectos, refs, memorización, Hooks personalizados, y cómo explicar los compromisos en las entrevistas.