Next.js
Estado global y stores en Next.js
Clasifica URL state, server state y client state, y explica cuándo usar Context, stores externas, query caches, persistencia y realtime en Next.js.
- Última actualización
- Actualizada
- Nivel
- Aplicación
Next.js
Clasifica URL state, server state y client state, y explica cuándo usar Context, stores externas, query caches, persistencia y realtime en Next.js.
Next.js no introduce un state manager propio. La decisión correcta comienza clasificando cada dato: URL state, server state, client state local, state compartido de una feature o state externo. La mayor parte de la complejidad aparece cuando una aplicación copia el mismo dato entre varias categorías.
URL state
→ pathname, params, search params
server state
→ database, API, session, cache, permissions
client UI state
→ modal, input draft, selection, disclosure
shared client state
→ cart drawer, editor context, realtime local model
external store
→ state outside React with subscriptionsAntes de instalar una store, pregunta dónde vive la fuente autoritativa y qué debe sobrevivir navegación, refresh o múltiples pestañas.
Filtros, tabs, búsqueda y paginación compartibles suelen pertenecer a la URL:
/products?category=coffee&page=2&sort=priceServer page:
export default async function ProductsPage({
searchParams,
}: PageProps<"/products">) {
const raw = await searchParams;
const filters = productFilterSchema.parse(raw);
const products = await searchProducts(filters);
return <Products products={products} filters={filters} />;
}Ventajas:
No dupliques esos filtros en useState salvo que exista un draft antes de confirmar la URL. Define cuándo se sincroniza.
Una lista de pedidos vive en DB/API y puede cambiar fuera de la pestaña.
Características:
En App Router, los Server Components y Cache Components son una estrategia natural para el estado inicial:
export default async function OrdersPage() {
const orders = await getAccessibleOrders();
return <OrdersTable orders={orders} />;
}No copies orders a Context solo para que un botón los lea. Pasa datos, compón componentes o usa una caché cliente si realmente necesitas mutaciones frecuentes sin navegación.
"use client";
export function Disclosure({ children }: { children: React.ReactNode }) {
const [open, setOpen] = useState(false);
return (
<section>
<button onClick={() => setOpen((value) => !value)} aria-expanded={open}>
Detalles
</button>
{open && children}
</section>
);
}Este state solo importa dentro del componente. Mantenerlo local reduce rerenders, contratos y bugs.
Un drawer de carrito puede necesitar controles en header, producto y checkout:
"use client";
const CartUIContext = createContext<CartUIContextValue | null>(null);
export function CartUIProvider({ children }: { children: React.ReactNode }) {
const [open, setOpen] = useState(false);
return (
<CartUIContext value={{ open, setOpen }}>
{children}
<CartDrawer />
</CartUIContext>
);
}El provider gestiona UI del carrito, no necesariamente la lista autoritativa de items. Los items pueden venir del servidor y las Actions actualizar tags.
Coloca el provider en el layout mínimo de shop, no en root si el resto de la app no lo usa.
Context distribuye un valor. Cuando cambia, todos los consumidores que leen ese Context pueden renderizar.
No ofrece automáticamente:
Para valores poco frecuentes como theme o sesión resumida puede ser suficiente.
Redux Toolkit, Zustand, Jotai u otras bibliotecas aportan distintos modelos.
Considera una store cuando:
No la elijas solo porque el proyecto “es grande”. Una aplicación grande puede tener state local bien delimitado.
Un singleton global servidor es peligroso:
// Avoid on server
export const store = createStore();Puede compartir state entre requests/usuarios en un proceso persistente.
Para SSR, crea una instancia por request o por boundary cliente:
"use client";
export function StoreProvider({
children,
initialState,
}: StoreProviderProps) {
const storeRef = useRef<AppStore | null>(null);
if (!storeRef.current) {
storeRef.current = createAppStore(initialState);
}
return <StoreContext value={storeRef.current}>{children}</StoreContext>;
}El provider cliente se instancia para el árbol del navegador. No muta durante render de forma no determinista.
El snapshot inicial del cliente debe coincidir con HTML:
server initialState
→ serialized minimal snapshot
→ client creates store once
→ hydration uses same valuesNo leas localStorage para el primer markup sin un snapshot estable. Puedes:
useSyncExternalStore con getServerSnapshot.SWR o TanStack Query aportan:
Úsalas cuando el dato debe vivir y actualizarse activamente en cliente:
Para una page que solo carga y navega, Server Components pueden ser más simples.
Puedes prefetchear en servidor y deshidratar la caché. Esto añade payload y coordinación. No hagas fetch en Server Component y otra vez en cliente sin una estrategia de hydration; producirás duplicación.
Elige una fuente por pantalla:
server-rendered data + Actions
or
client query cache hydrated from serverPueden coexistir por regiones, pero documenta ownership.
const total = items.reduce((sum, item) => sum + item.price * item.quantity, 0);No guardes total en otra store si puede calcularse. Menos state reduce desincronización.
Memoiza solo si el cálculo medido lo requiere.
Antes de Context:
export function DashboardLayout({
sidebar,
children,
}: {
sidebar: React.ReactNode;
children: React.ReactNode;
}) {
return (
<div>
<aside>{sidebar}</aside>
<main>{children}</main>
</div>
);
}Composición evita que el layout conozca el tipo de los datos y permite que Server Components produzcan el contenido.
Para state compartible.
Preferencias servidor/locale/theme; tamaño pequeño.
Preferencias cliente no sensibles; no disponible en servidor.
Datos offline/voluminosos.
State importante entre dispositivos.
Persistir todo puede restaurar versiones incompatibles o datos sensibles. Versiona el schema y define migración/expiración.
localStorage events, BroadcastChannel o una store externa pueden sincronizar pestañas. Esto no reemplaza el servidor autoritativo.
Caso: logout en una pestaña debe invalidar sesión servidor y avisar a las otras para actualizar UI.
Un WebSocket/SSE entrega eventos:
server event
→ update query cache/store
→ renderNo guardes cada evento histórico en state si solo necesitas el snapshot actual. Diseña reconnect, orden, duplicados y resync.
URL state
→ branch, date range, filters
server state
→ inventory, orders, users
local client state
→ open modal, selected row draft
feature context
→ POS checkout UI
external store
→ only if cross-feature high-frequency state proves necessaryUna venta usa Server Action/transacción y revalidation. La UI puede usar optimismo, pero inventario confirmado viene del servidor.
Problemas comunes:
Mide con React Profiler y divide providers por responsabilidad/frecuencia.
No memoices todo como primera respuesta.
Nunca almacenes como autoridad cliente:
El cliente puede manipular store/localStorage. El servidor vuelve a validar.
Recrea cache, permisos y revalidation manualmente.
Mezcla requests.
Amplía bundle y renders.
Mismatch/flash.
URL, props y store divergen.
Más estados inválidos.
No autoriza.
Manejo de errores, resiliencia y recuperación diseña cómo cada frontera falla sin derribar toda la aplicación ni ocultar problemas reales.