Esta página fue traducida por PageTurner AI (beta). No está respaldada oficialmente por el proyecto. ¿Encontraste un error? Reportar problema →
Configuración de Redux Toolkit con Next.js
- Cómo configurar y usar Redux Toolkit con el framework Next.js
- Conocimiento de sintaxis y características de ES2015
- Familiaridad con terminología de React: JSX, Estado, Componentes funcionales, Props y Hooks
- Comprensión de términos y conceptos de Redux
- Se recomienda completar los tutoriales Inicio rápido y TypeScript Inicio rápido, e idealmente también el tutorial completo de Conceptos esenciales de Redux
Introducción
Next.js es un popular framework de renderizado del lado del servidor para React que presenta desafíos únicos al usar Redux correctamente. Estos desafíos incluyen:
-
Creación segura de store Redux por solicitud: Un servidor Next.js puede manejar múltiples solicitudes simultáneamente. Esto significa que el store de Redux debe crearse por solicitud y no debe compartirse entre solicitudes.
-
Hidratación del store compatible con SSR: Las aplicaciones Next.js se renderizan dos veces, primero en el servidor y luego en el cliente. Si no se renderiza el mismo contenido en cliente y servidor, se producirá un "error de hidratación". Por lo tanto, el store de Redux debe inicializarse en el servidor y luego reinicializarse en el cliente con los mismos datos para evitar problemas de hidratación.
-
Soporte para enrutamiento SPA: Next.js utiliza un modelo híbrido para el enrutamiento del lado del cliente. La primera carga de página recibe un resultado SSR del servidor. Las navegaciones posteriores se manejan en el cliente. Esto implica que con un store singleton definido en el layout, los datos específicos de ruta deberán reiniciarse selectivamente durante la navegación, mientras que los datos no específicos de ruta deberán conservarse en el store.
-
Compatible con caché de servidor: Las versiones recientes de Next.js (específicamente aplicaciones que usan la arquitectura App Router) admiten un almacenamiento en caché agresivo. La arquitectura ideal del store debe ser compatible con este caché.
Existen dos arquitecturas para aplicaciones Next.js: el Pages Router y el App Router.
El Pages Router es la arquitectura original de Next.js. Si usas Pages Router, la configuración de Redux se maneja principalmente con la biblioteca next-redux-wrapper, que integra un store Redux con métodos de obtención de datos del Pages Router como getServerSideProps.
Esta guía se centrará en la arquitectura App Router, ya que es la nueva opción predeterminada en Next.js.
Cómo leer esta guía
Esta página asume que ya tienes una aplicación Next.js existente basada en la arquitectura App Router.
Si quieres seguir los ejemplos, puedes crear un nuevo proyecto Next vacío con npx create-next-app my-app; las opciones predeterminadas configurarán un proyecto nuevo con App Router habilitado. Luego, añade @reduxjs/toolkit y react-redux como dependencias.
También puedes crear un proyecto Next+Redux con npx create-next-app --example with-redux my-app, que incluye las configuraciones iniciales descritas en esta página.
La arquitectura App Router y Redux
La principal característica nueva del App Router de Next.js es la adición de soporte para React Server Components (RSCs). Los RSCs son un tipo especial de componente React que solo se renderiza en el servidor, a diferencia de los componentes "cliente" que se renderizan tanto en el cliente como en el servidor. Los RSCs pueden definirse como funciones async y devolver promesas durante el renderizado mientras realizan solicitudes asíncronas para obtener datos.
La capacidad de los RSCs para bloquear solicitudes de datos significa que con el App Router ya no tienes getServerSideProps para obtener datos para el renderizado. Cualquier componente en el árbol puede realizar solicitudes asíncronas de datos. Aunque esto es muy conveniente, también implica que si defines variables globales (como el store de Redux), se compartirán entre solicitudes. Esto es un problema porque el store de Redux podría contaminarse con datos de otras solicitudes.
Basándonos en la arquitectura del App Router, tenemos estas recomendaciones generales para el uso adecuado de Redux:
-
No usar stores globales: Debido a que el store de Redux se comparte entre solicitudes, no debe definirse como una variable global. En su lugar, se debe crear un store por solicitud.
-
Los RSCs no deben leer ni escribir en el store de Redux: Los RSCs no pueden usar hooks ni context. No están diseñados para ser stateful. Que un RSC lea o escriba valores desde un store global viola la arquitectura del App Router de Next.js.
-
El store solo debe contener datos mutables: Recomendamos usar Redux de forma limitada para datos que deben ser globales y mutables.
Estas recomendaciones son específicas para aplicaciones desarrolladas con el App Router de Next.js. Las Single Page Applications (SPAs) no se ejecutan en el servidor y por tanto pueden definir stores como variables globales. Las SPAs no necesitan preocuparse por los RSCs ya que no existen en ellas. Y los stores singleton pueden almacenar cualquier dato que desees.
Estructura de carpetas
Las aplicaciones Next pueden crearse con la carpeta /app en la raíz o anidada bajo /src/app. Tu lógica de Redux debe ir en una carpeta separada, junto a la carpeta /app. Es común poner la lógica de Redux en una carpeta llamada /lib, pero no es obligatorio.
La estructura de archivos y carpetas dentro de /lib depende de ti, pero generalmente recomendamos una estructura basada en "feature folders" para la lógica de Redux.
Un ejemplo típico podría verse así:
/app
layout.tsx
page.tsx
StoreProvider.tsx
/lib
store.ts
/features
/todos
todosSlice.ts
Usaremos este enfoque en esta guía.
Configuración inicial
Similar al Tutorial de TypeScript de RTK, necesitamos crear un archivo para el store de Redux, así como los tipos inferidos RootState y AppDispatch.
Sin embargo, la arquitectura multipágina de Next requiere algunas diferencias respecto a la configuración de una aplicación de página única.
Creación de un store de Redux por solicitud
El primer cambio es pasar de definir store como una variable global o singleton de módulo, a definir una función makeStore que devuelva un nuevo store por cada solicitud:
- TypeScript
- JavaScript
import { configureStore } from '@reduxjs/toolkit'
export const makeStore = () => {
return configureStore({
reducer: {}
})
}
// Infer the type of makeStore
export type AppStore = ReturnType<typeof makeStore>
// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<AppStore['getState']>
export type AppDispatch = AppStore['dispatch']
import { configureStore } from '@reduxjs/toolkit'
export const makeStore = () => {
return configureStore({
reducer: {}
})
}
Ahora tenemos una función makeStore que podemos usar para crear una instancia del store por solicitud, manteniendo la seguridad de tipos (si eliges usar TypeScript) que proporciona Redux Toolkit.
No exportamos una variable store, pero podemos inferir los tipos RootState y AppDispatch a partir del tipo de retorno de makeStore.
También querrás crear y exportar versiones pre-tipadas de los hooks de React-Redux para simplificar su uso posterior:
- TypeScript
- JavaScript
import { useDispatch, useSelector, useStore } from 'react-redux'
import type { AppDispatch, AppStore, RootState } from './store'
// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
export const useAppSelector = useSelector.withTypes<RootState>()
export const useAppStore = useStore.withTypes<AppStore>()
import { useDispatch, useSelector, useStore } from 'react-redux'
// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch = useDispatch.withTypes()
export const useAppSelector = useSelector.withTypes()
export const useAppStore = useStore.withTypes()
Proporcionando el Store
Para usar esta nueva función makeStore, necesitamos crear un nuevo componente "cliente" que cree el store y lo comparta usando el componente Provider de React-Redux.
- TypeScript
- JavaScript
'use client'
import { useRef } from 'react'
import { Provider } from 'react-redux'
import { makeStore, AppStore } from '../lib/store'
export default function StoreProvider({
children
}: {
children: React.ReactNode
}) {
const storeRef = useRef<AppStore | null>(null)
if (!storeRef.current) {
// Create the store instance the first time this renders
storeRef.current = makeStore()
}
return <Provider store={storeRef.current}>{children}</Provider>
}
'use client'
import { useRef } from 'react'
import { Provider } from 'react-redux'
import { makeStore } from '../lib/store'
export default function StoreProvider({ children }) {
const storeRef = useRef(null)
if (!storeRef.current) {
// Create the store instance the first time this renders
storeRef.current = makeStore()
}
return <Provider store={storeRef.current}>{children}</Provider>
}
En este código de ejemplo nos aseguramos de que este componente cliente sea seguro para la re-renderización comprobando el valor de la referencia para garantizar que el store solo se crea una vez. Este componente se renderizará una sola vez por solicitud en el servidor, pero podría re-renderizarse múltiples veces en el cliente si hay componentes cliente con estado ubicados arriba en el árbol, o si este componente también contiene otro estado mutable que provoque una re-renderización.
Cualquier componente que interactúe con el store de Redux (creándolo, proporcionándolo, leyendo o escribiendo en él) debe ser un componente cliente. Esto se debe a que acceder al store requiere el contexto de React, y el contexto solo está disponible en componentes cliente.
El siguiente paso es incluir StoreProvider en cualquier parte del árbol superior donde se use el store. Puedes ubicar el store en el componente layout si todas las rutas que usan ese layout necesitan el store. O si el store solo se usa en una ruta específica, puedes crearlo y proporcionarlo en ese manejador de ruta. En todos los componentes cliente inferiores del árbol, puedes usar el store normalmente con los hooks proporcionados por react-redux.
Carga de datos iniciales
Si necesitas inicializar el store con datos del componente padre, define esos datos como una prop en el componente cliente StoreProvider y usa una acción de Redux en el slice para establecer los datos en el store como se muestra a continuación.
- TypeScript
- JavaScript
'use client'
import { useRef } from 'react'
import { Provider } from 'react-redux'
import { makeStore, AppStore } from '../lib/store'
import { initializeCount } from '../lib/features/counter/counterSlice'
export default function StoreProvider({
count,
children
}: {
count: number
children: React.ReactNode
}) {
const storeRef = useRef<AppStore | null>(null)
if (!storeRef.current) {
storeRef.current = makeStore()
storeRef.current.dispatch(initializeCount(count))
}
return <Provider store={storeRef.current}>{children}</Provider>
}
'use client'
import { useRef } from 'react'
import { Provider } from 'react-redux'
import { makeStore } from '../lib/store'
import { initializeCount } from '../lib/features/counter/counterSlice'
export default function StoreProvider({ count, children }) {
const storeRef = useRef(null)
if (!storeRef.current) {
storeRef.current = makeStore()
storeRef.current.dispatch(initializeCount(count))
}
return <Provider store={storeRef.current}>{children}</Provider>
}
Configuración adicional
Estado por ruta
Si usas la navegación estilo SPA en el cliente de Next.js mediante next/navigation, cuando los usuarios navegan entre páginas solo se re-renderizará el componente de ruta. Esto significa que si tienes un store de Redux creado y proporcionado en el componente layout, se conservará entre cambios de ruta. No es un problema si solo usas el store para datos globales mutables. Sin embargo, si usas el store para datos específicos de ruta, deberás restablecer esos datos cuando cambie la ruta.
A continuación se muestra un componente de ejemplo ProductName que usa el store de Redux para gestionar el nombre mutable de un producto. ProductName forma parte de una ruta de detalle de producto. Para asegurarnos de tener el nombre correcto en el store, debemos establecer el valor cada vez que el componente ProductName se renderiza inicialmente, lo que ocurre en cada cambio de ruta hacia la página de detalle.
- TypeScript
- JavaScript
'use client'
import { useRef } from 'react'
import { useAppSelector, useAppDispatch, useAppStore } from '../lib/hooks'
import {
initializeProduct,
setProductName,
Product
} from '../lib/features/product/productSlice'
export default function ProductName({ product }: { product: Product }) {
// Initialize the store with the product information
const store = useAppStore()
const initialized = useRef(false)
if (!initialized.current) {
store.dispatch(initializeProduct(product))
initialized.current = true
}
const name = useAppSelector(state => state.product.name)
const dispatch = useAppDispatch()
return (
<input
value={name}
onChange={e => dispatch(setProductName(e.target.value))}
/>
)
}
'use client'
import { useRef } from 'react'
import { useAppSelector, useAppDispatch, useAppStore } from '../lib/hooks'
import {
initializeProduct,
setProductName
} from '../lib/features/product/productSlice'
export default function ProductName({ product }) {
// Initialize the store with the product information
const store = useAppStore()
const initialized = useRef(false)
if (!initialized.current) {
store.dispatch(initializeProduct(product))
initialized.current = true
}
const name = useAppSelector(state => state.product.name)
const dispatch = useAppDispatch()
return (
<input
value={name}
onChange={e => dispatch(setProductName(e.target.value))}
/>
)
}
Aquí usamos el mismo patrón de inicialización anterior, despachando acciones al store para establecer los datos específicos de ruta. La referencia initialized garantiza que el store solo se inicialice una vez por cambio de ruta.
Vale la pena señalar que inicializar el store con useEffect no funcionaría porque useEffect solo se ejecuta en el cliente. Esto causaría errores de hidratación o parpadeo porque el resultado del renderizado en el servidor no coincidiría con el del cliente.
Caché
El App Router tiene cuatro cachés separadas, incluyendo cachés de solicitudes fetch y de rutas. La caché que más probabilidades tiene de causar problemas es la de rutas. Si tu aplicación acepta inicio de sesión y tiene rutas (ej. la ruta principal /) que muestran datos diferentes según el usuario, deberás desactivar la caché de ruta usando dynamic en el manejador de ruta:
- TypeScript
- JavaScript
export const dynamic = 'force-dynamic'
export const dynamic = 'force-dynamic'
Después de una mutación, también deberías invalidar la caché llamando a revalidatePath o revalidateTag según corresponda.
RTK Query
Recomendamos usar RTK Query para la obtención de datos solo en el cliente. La obtención de datos en el servidor debe usar solicitudes fetch desde RSCs async.
Puedes aprender más sobre RTK Query en el tutorial de RTK Query.
En el futuro, RTK Query podría recibir datos obtenidos en el servidor a través de React Server Components, pero es una capacidad futura que requerirá cambios tanto en React como en RTK Query.
Comprobando tu trabajo
Hay tres áreas clave que deberías comprobar para asegurarte de que has configurado Redux Toolkit correctamente:
-
Server Side Rendering — Comprueba el HTML generado por el servidor para asegurarte de que los datos del store de Redux están presentes en el resultado renderizado en el servidor.
-
Cambio de ruta — Navega entre páginas de la misma ruta y entre rutas diferentes para asegurarte de que los datos específicos de cada ruta se inicializan correctamente.
-
Mutaciones — Verifica que el store sea compatible con las cachés del App Router de Next.js realizando una mutación, luego navegando fuera de la ruta y volviendo a la ruta original para comprobar que los datos se actualizan.
Recomendaciones generales
El App Router presenta una arquitectura radicalmente diferente para aplicaciones React, tanto respecto al Pages Router como a una aplicación SPA. Recomendamos reconsiderar tu enfoque de gestión del estado a la luz de esta nueva arquitectura. En aplicaciones SPA es habitual tener un store grande que contenga todos los datos, tanto mutables como inmutables, necesarios para impulsar la aplicación. Para aplicaciones con App Router recomendamos que:
-
utilices Redux solo para datos mutables compartidos globalmente
-
uses una combinación del estado de Next.js (parámetros de búsqueda, parámetros de ruta, estado de formularios, etc.), contexto de React y hooks de React para el resto de la gestión del estado.
Lo que has aprendido
Este ha sido un breve resumen de cómo configurar y usar Redux Toolkit con el App Router:
- Crea un store de Redux por petición usando
configureStoreencapsulado en una funciónmakeStore - Proporciona el store de Redux a los componentes de React usando un componente "cliente"
- Interactúa con el store de Redux solo en componentes cliente porque solo los componentes cliente tienen acceso al contexto de React
- Usa el store como lo harías normalmente con los hooks proporcionados por React-Redux
- Debes tener en cuenta el caso donde tienes estado específico por ruta en un store global ubicado en el layout
¿Qué sigue?
Recomendamos revisar los tutoriales "Redux Essentials" y "Redux Fundamentals" en la documentación principal de Redux, que te darán una comprensión completa de cómo funciona Redux, qué hace Redux Toolkit y cómo usarlo correctamente.