Cómo construir aplicaciones de una sola página con Next.js
Next.js soporta completamente la construcción de Aplicaciones de Una Sola Página (SPAs).
Esto incluye transiciones rápidas entre rutas con prefetching, obtención de datos del lado del cliente, uso de APIs del navegador, integración con bibliotecas de terceros para el cliente, creación de rutas estáticas y más.
Si ya tiene una SPA existente, puede migrarla a Next.js sin realizar grandes cambios en su código. Next.js le permite añadir progresivamente características del servidor según sea necesario.
¿Qué es una Aplicación de Una Sola Página?
La definición de una SPA varía. Definiremos una "SPA estricta" como:
- Renderizado del lado del cliente (CSR): La aplicación se sirve mediante un único archivo HTML (ej.
index.html
). Cada ruta, transición de página y obtención de datos se maneja mediante JavaScript en el navegador. - Sin recargas completas de página: En lugar de solicitar un nuevo documento para cada ruta, el JavaScript del cliente manipula el DOM de la página actual y obtiene los datos según sea necesario.
Las SPAs estrictas suelen requerir grandes cantidades de JavaScript para cargar antes de que la página pueda ser interactiva. Además, las cascadas de datos del cliente pueden ser difíciles de gestionar. Construir SPAs con Next.js puede abordar estos problemas.
¿Por qué usar Next.js para SPAs?
Next.js puede dividir automáticamente sus paquetes de JavaScript y generar múltiples puntos de entrada HTML para diferentes rutas. Esto evita cargar código JavaScript innecesario en el cliente, reduciendo el tamaño del paquete y permitiendo cargas de página más rápidas.
El componente next/link
precarga automáticamente las rutas, ofreciéndole las rápidas transiciones de página de una SPA estricta, pero con la ventaja de persistir el estado de enrutamiento de la aplicación en la URL para compartir y enlazar.
Next.js puede comenzar como un sitio estático o incluso una SPA estricta donde todo se renderiza del lado del cliente. Si su proyecto crece, Next.js le permite añadir progresivamente más características del servidor (ej. Componentes del Servidor de React, Acciones del Servidor, y más) según sea necesario.
Ejemplos
Exploremos patrones comunes utilizados para construir SPAs y cómo Next.js los resuelve.
Usar el hook use
de React dentro de un Proveedor de Contexto
Recomendamos obtener datos en un componente padre (o layout), devolver la Promesa, y luego desempaquetar el valor en un Componente del Cliente con el hook use
de React.
Next.js puede comenzar a obtener datos temprano en el servidor. En este ejemplo, ese es el layout raíz — el punto de entrada a su aplicación. El servidor puede comenzar inmediatamente a transmitir una respuesta al cliente.
Al "elevar" su obtención de datos al layout raíz, Next.js inicia las solicitudes especificadas en el servidor antes que cualquier otro componente en su aplicación. Esto elimina cascadas del cliente y evita múltiples idas y vueltas entre cliente y servidor. También puede mejorar significativamente el rendimiento, ya que su servidor está más cerca (y idealmente colocado) de donde se encuentra su base de datos.
Por ejemplo, actualice su layout raíz para llamar a la Promesa, pero no la espere.
import { UserProvider } from './user-provider'
import { getUser } from './user' // alguna función del lado del servidor
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
let userPromise = getUser() // NO usar await
return (
<html lang="en">
<body>
<UserProvider userPromise={userPromise}>{children}</UserProvider>
</body>
</html>
)
}
import { UserProvider } from './user-provider'
import { getUser } from './user' // alguna función del lado del servidor
export default function RootLayout({ children }) {
let userPromise = getUser() // NO usar await
return (
<html lang="en">
<body>
<UserProvider userPromise={userPromise}>{children}</UserProvider>
</body>
</html>
)
}
Mientras puede diferir y pasar una sola Promesa como prop a un Componente del Cliente, generalmente vemos este patrón emparejado con un proveedor de contexto de React. Esto permite un acceso más fácil desde Componentes del Cliente con un Hook personalizado de React.
Puede reenviar una Promesa al proveedor de contexto de React:
'use client';
import { createContext, useContext, ReactNode } from 'react';
type User = any;
type UserContextType = {
userPromise: Promise<User | null>;
};
const UserContext = createContext<UserContextType | null>(null);
export function useUser(): UserContextType {
let context = useContext(UserContext);
if (context === null) {
throw new Error('useUser debe usarse dentro de un UserProvider');
}
return context;
}
export function UserProvider({
children,
userPromise
}: {
children: ReactNode;
userPromise: Promise<User | null>;
}) {
return (
<UserContext.Provider value={{ userPromise }}>
{children}
</UserContext.Provider>
);
}
'use client'
import { createContext, useContext, ReactNode } from 'react'
const UserContext = createContext(null)
export function useUser() {
let context = useContext(UserContext)
if (context === null) {
throw new Error('useUser debe usarse dentro de un UserProvider')
}
return context
}
export function UserProvider({ children, userPromise }) {
return (
<UserContext.Provider value={{ userPromise }}>
{children}
</UserContext.Provider>
)
}
Finalmente, puede llamar al hook personalizado useUser()
en cualquier Componente del Cliente y desempaquetar la Promesa:
'use client'
import { use } from 'react'
import { useUser } from './user-provider'
export function Profile() {
const { userPromise } = useUser()
const user = use(userPromise)
return '...'
}
'use client'
import { use } from 'react'
import { useUser } from './user-provider'
export function Profile() {
const { userPromise } = useUser()
const user = use(userPromise)
return '...'
}
El componente que consume la Promesa (ej. Profile
arriba) será suspendido. Esto permite hidratación parcial. Puede ver el HTML transmitido y prerenderizado antes de que JavaScript haya terminado de cargar.
SPAs con SWR
SWR es una biblioteca popular de React para obtención de datos.
Con SWR 2.3.0 (y React 19+), puede adoptar gradualmente características del servidor junto con su código existente de obtención de datos del cliente basado en SWR. Esto es una abstracción del patrón use()
anterior. Significa que puede mover la obtención de datos entre el cliente y el servidor, o usar ambos:
- Solo cliente:
useSWR(key, fetcher)
- Solo servidor:
useSWR(key)
+ datos proporcionados por RSC - Mixto:
useSWR(key, fetcher)
+ datos proporcionados por RSC
Por ejemplo, envuelva su aplicación con <SWRConfig>
y un fallback
:
import { SWRConfig } from 'swr'
import { getUser } from './user' // alguna función del lado del servidor
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<SWRConfig
value={{
fallback: {
// NO usamos await getUser() aquí
// Solo los componentes que lean estos datos se suspenderán
'/api/user': getUser(),
},
}}
>
{children}
</SWRConfig>
)
}
import { SWRConfig } from 'swr'
import { getUser } from './user' // alguna función del lado del servidor
export default function RootLayout({ children }) {
return (
<SWRConfig
value={{
fallback: {
// NO usamos await getUser() aquí
// Solo los componentes que lean estos datos se suspenderán
'/api/user': getUser(),
},
}}
>
{children}
</SWRConfig>
)
}
Como este es un Componente del Servidor, getUser()
puede leer cookies, encabezados o hablar con su base de datos de forma segura. No se necesita una ruta API separada. Los componentes del cliente debajo de <SWRConfig>
pueden llamar a useSWR()
con la misma clave para recuperar los datos del usuario. El código del componente con useSWR
no requiere ningún cambio respecto a su solución existente de obtención del cliente.
'use client'
import useSWR from 'swr'
export function Profile() {
const fetcher = (url) => fetch(url).then((res) => res.json())
// El mismo patrón SWR que ya conoce
const { data, error } = useSWR('/api/user', fetcher)
return '...'
}
'use client'
import useSWR from 'swr'
export function Profile() {
const fetcher = (url) => fetch(url).then((res) => res.json())
// El mismo patrón SWR que ya conoce
const { data, error } = useSWR('/api/user', fetcher)
return '...'
}
Los datos de fallback
pueden prerenderizarse e incluirse en la respuesta HTML inicial, luego leerse inmediatamente en los componentes hijos usando useSWR
. El sondeo, revalidación y caché de SWR aún se ejecutan solo del lado del cliente, por lo que preserva toda la interactividad que necesita para una SPA.
Como los datos iniciales de fallback
son manejados automáticamente por Next.js, ahora puede eliminar cualquier lógica condicional previamente necesaria para verificar si data
era undefined
. Cuando los datos se están cargando, se suspenderá el límite <Suspense>
más cercano.
SWR | RSC | RSC + SWR | |
---|---|---|---|
Datos SSR | |||
Transmisión durante SSR | |||
Desduplicar solicitudes | |||
Características del cliente |
SPAs con React Query
Puede usar React Query con Next.js tanto en el cliente como en el servidor. Esto le permite construir tanto SPAs estrictas, como aprovechar características del servidor en Next.js junto con React Query.
Aprenda más en la documentación de React Query.
Renderizar componentes solo en el navegador
Los componentes del cliente son prerenderizados durante next build
. Si desea desactivar el prerenderizado para un Componente del Cliente y solo cargarlo en el entorno del navegador, puede usar next/dynamic
:
import dynamic from 'next/dynamic'
const ClientOnlyComponent = dynamic(() => import('./component'), {
ssr: false,
})
Esto puede ser útil para bibliotecas de terceros que dependen de APIs del navegador como window
o document
. También puede añadir un useEffect
que verifique la existencia de estas APIs, y si no existen, devolver null
o un estado de carga que sería prerenderizado.
Enrutamiento superficial en el cliente
Si está migrando desde una SPA estricta como Create React App o Vite, podría tener código existente que enruta superficialmente para actualizar el estado de la URL. Esto puede ser útil para transiciones manuales entre vistas en su aplicación sin usar el enrutamiento basado en archivos predeterminado de Next.js.
Next.js le permite usar los métodos nativos window.history.pushState
y window.history.replaceState
para actualizar el historial del navegador sin recargar la página.
Las llamadas pushState
y replaceState
se integran con el Enrutador de Next.js, permitiéndole sincronizar con usePathname
y useSearchParams
.
'use client'
import { useSearchParams } from 'next/navigation'
export default function SortProducts() {
const searchParams = useSearchParams()
function updateSorting(sortOrder: string) {
const urlSearchParams = new URLSearchParams(searchParams.toString())
urlSearchParams.set('sort', sortOrder)
window.history.pushState(null, '', `?${urlSearchParams.toString()}`)
}
return (
<>
<button onClick={() => updateSorting('asc')}>Ordenar Ascendente</button>
<button onClick={() => updateSorting('desc')}>Ordenar Descendente</button>
</>
)
}
'use client'
import { useSearchParams } from 'next/navigation'
export default function SortProducts() {
const searchParams = useSearchParams()
function updateSorting(sortOrder) {
const urlSearchParams = new URLSearchParams(searchParams.toString())
urlSearchParams.set('sort', sortOrder)
window.history.pushState(null, '', `?${urlSearchParams.toString()}`)
}
return (
<>
<button onClick={() => updateSorting('asc')}>Ordenar Ascendente</button>
<button onClick={() => updateSorting('desc')}>Ordenar Descendente</button>
</>
)
}
Aprenda más sobre cómo funcionan el enrutamiento y navegación en Next.js.
Usar Acciones del Servidor en Componentes del Cliente
Puede adoptar progresivamente Acciones del Servidor mientras sigue usando Componentes del Cliente. Esto le permite eliminar código repetitivo para llamar a una ruta API, y en su lugar usar características de React como useActionState
para manejar estados de carga y error.
Por ejemplo, cree su primera Acción del Servidor:
'use server'
export async function create() {}
'use server'
export async function create() {}
Puede importar y usar una Acción del Servidor desde el cliente, similar a llamar a una función JavaScript. No necesita crear manualmente un endpoint API:
Aprenda más sobre mutar datos con Acciones del Servidor.
Exportación estática (opcional)
Next.js también soporta generar un sitio completamente estático. Esto tiene algunas ventajas sobre las SPAs estrictas:
- División de código automática: En lugar de enviar un solo
index.html
, Next.js generará un archivo HTML por ruta, por lo que sus visitantes obtienen el contenido más rápido sin esperar el paquete JavaScript del cliente. - Mejor experiencia de usuario: En lugar de un esqueleto mínimo para todas las rutas, obtiene páginas completamente renderizadas para cada ruta. Cuando los usuarios navegan del lado del cliente, las transiciones siguen siendo instantáneas y similares a una SPA.
Para habilitar una exportación estática, actualice su configuración:
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
output: 'export',
}
export default nextConfig
Después de ejecutar next build
, Next.js creará una carpeta out
con los activos HTML/CSS/JS para su aplicación.
Nota: Las características del servidor de Next.js no son compatibles con exportaciones estáticas. Aprenda más.
Migrar proyectos existentes a Next.js
Puede migrar incrementalmente a Next.js siguiendo nuestras guías:
Si ya está usando una SPA con el Pages Router, puede aprender cómo adoptar incrementalmente el App Router.
Autoalojamiento
Aprende cómo autoalojar tu aplicación Next.js en un servidor Node.js, imagen Docker o archivos HTML estáticos (exportaciones estáticas).
Exportaciones estáticas
Next.js permite comenzar como un sitio estático o una Aplicación de Página Única (SPA), y luego actualizar opcionalmente para usar funciones que requieran un servidor.