Entendé loading.tsx en Next.js App Router: UI de carga automática con Suspense
Introducción
En el ecosistema de React, mostrar una interfaz de carga mientras se obtienen datos es una tarea cotidiana. Con la llegada del App Router de Next.js 13+, el framework introdujo una convención poderosa: el archivo loading.tsx. Este archivo permite definir una UI de carga que se muestra automáticamente gracias al mecanismo de Suspense de React, sin necesidad de envolver manualmente cada componente.
Este artículo está dirigido a desarrolladores que ya conocen los conceptos básicos de Next.js y React, y que quieren aprovechar al máximo el App Router para crear experiencias de usuario fluidas. Aprenderás:
- Qué es
loading.tsxy cómo se integra con el enrutamiento basado en carpetas. - Cómo Next.js aplica Suspense de forma automática a los segmentos de ruta.
- Qué ocurre cuando tienes varios niveles de
loading.tsxen una ruta anidada. - Cómo personalizar la UI de carga con spinners, esqueletos o cualquier otro diseño.
- La relación entre
loading.tsxyerror.tsx. - Buenas prácticas y errores comunes que debés evitar.
Al final, tendrás ejemplos listos para copiar y pegar, con la salida esperada comentada, para que veas exactamente qué se renderiza en cada situación.
¿Cómo funciona Suspense en el App Router?
Antes de Next.js 13, si querías usar Suspense tenías que envolver tus componentes en un <Suspense fallback={...}> manualmente. En el App Router, Next.js hace eso por vos: cada segmento de ruta (es decir, cada carpeta que representa un nivel de la URL) se trata como un límite de Suspense implícito.
Cuando un componente dentro de ese segmento lanza una promesa (por ejemplo, al llamar a una función async que aún no se resolvió), React interrumpe la renderización y muestra el fallback más cercano que encuentre en el árbol. Ese fallback proviene del archivo loading.tsx ubicado en el mismo segmento o en uno superior.
Este comportamiento se activa automáticamente; no necesitás importar nada ni escribir código extra. Basta con colocar un archivo llamado loading.tsx dentro de la carpeta del segmento que querés "cubrir" con una UI de carga.
Estructura de carpetas y el rol de loading.tsx
Supongamos la siguiente estructura de rutas:
app/
layout.tsx
page.tsx # ruta / (home)
dashboard/
layout.tsx
page.tsx # ruta /dashboard
loading.tsx # UI de carga para /dashboard y sus hijos
analytics/
page.tsx # ruta /dashboard/analyticsCuando el usuario navega a /dashboard/analytics, Next.js renderiza:
- El
layout.tsxde la raíz. - El
layout.tsxde/dashboard. - El
loading.tsxde/dashboardmientras el segmentoanalyticsno haya terminado de cargar sus datos. - Una vez que la promesa del segmento
analyticsse resuelve, elloading.tsxse reemplaza por elpage.tsxdeanalytics.
Si también existiera un loading.tsx dentro de app/dashboard/analytics/, ese tendría prioridad sobre el del padre mientras se carga ese segmento específico.
Primer ejemplo: loading.tsx sencillo
Vamos a crear una página que simule una solicitud lenta con await new Promise(res => setTimeout(res, 2000));.
Archivo app/page.tsx
import { notFound } from 'next/navigation';
export default async function Page() {
// Simulamos una demora de 2 segundos
await new Promise(res => setTimeout(res, 2000));
return (
<div>
<h1>Bienvenido a la home</h1>
<p>Datos cargados después de 2 segundos.</p>
</div>
);
}Archivo app/loading.tsx
export default function Loading() {
return (
<div className='flex min-h-[80vh] items-center justify-center'>
<span className='animate-pulse'>Cargando…</span>
</div>
);
}Qué ves en el navegador
Al ingresar a /, la pantalla queda en blanco durante unos milisegundos mientras Next.js resuelve la ruta. Luego, como el componente Page está tardando 2 segundos en devolver su JSX, se muestra el fallback definido en loading.tsx. Pasados los 2 segundos, el contenido de page.tsx reemplaza al loading.
Salida esperada (comentada):
// t=0ms
// → se muestra: <div class='flex min-h-[80vh] items-center justify-center'><span class='animate-pulse'>Cargando…</span></div>
// t=2000ms
// → se muestra: <div><h1>Bienvenido a la home</h1><p>Datos cargados después de 2 segundos.</p></div>Loading anidado: múltiples niveles
Ahora vamos a ver cómo se comportan varios loading.tsx cuando hay rutas anidadas.
Estructura
app/
dashboard/
loading.tsx # loading del panel
analytics/
loading.tsx # loading específico de analytics
page.tsxapp/dashboard/loading.tsx
export default function Loading() {
return (
<div className='p-4 bg-gray-50'>
<h2 className='text-lg'>Cargando panel…</h2>
</div>
);
}app/dashboard/analytics/loading.tsx
export default function Loading() {
return (
<div className='p-4 bg-blue-50'>
<h2 className='text-lg'>Cargando analytics…</h2>
</div>
);
}app/dashboard/analytics/page.tsx
export default async function Page() {
await new Promise(res => setTimeout(res, 3000));
return <p>Analytics listos después de 3 segundos.</p>;
}Flujo de renderizado
- Al entrar a
/dashboard/analytics, Next.js muestra primero elloading.tsxdedashboardporque el segmentodashboardaún no ha terminado de cargar su layout (aunque no tenga datos, el hecho de que haya un hijo pendiente activa el Suspense). - Mientras tanto, el segmento
analyticsempieza a cargar su propia promesa (3 segundos). - Como hay un
loading.tsxmás específico (el deanalytics), Next.js reemplaza el loading del padre por el del hijo tan pronto como detecta que el hijo está en su fase de carga. - Pasados 3 segundos, la promesa de
analyticsse resuelve y se muestra el contenido depage.tsx.
Salida esperada (comentada):
// t=0ms
→ loading de dashboard (bg-gray-50)
// t≈0ms (inmediatamente después de detectar que analytics está cargando)
→ loading de analytics (bg-blue-50) <-- reemplaza al anterior
// t=3000ms
→ <p>Analytics listos después de 3 segundos.</p>Si quitáramos el loading.tsx de analytics, el loading de dashboard permanecería visible durante los 3 segundos completos.
Personalizando la UI de carga
El archivo loading.tsx puede contener cualquier JSX: spinners, esqueletos, mensajes, animaciones, etc. A continuación, algunos ejemplos prácticos.
1. Spinner con Tailwind CSS y animación de keyframes
export default function Loading() {
return (
<div className='flex min-h-[80vh] items-center justify-center'>
<div className='w-12 h-12 border-4 border-t-4 border-blue-500 rounded-full animate-spin'></div>
</div>
);
}2. Esqueleto de tarjeta (skeleton UI)
Supongamos que la página mostrará una lista de tarjetas. Mientras cargamos, mostramos versiones grises de esas tarjetas.
export default function Loading() {
return (
<div className='space-y-4 p-6'>
{[1,2,3].map(i => (
<div key={i} className='flex space-x-4'>
<div className='w-16 h-16 bg-gray-300 rounded-full animate-pulse'></div>
<div className='space-y-2'>
<div className='h-4 w-32 bg-gray-300 rounded animate-pulse'></div>
<div className='h-4 w-48 bg-gray-300 rounded animate-pulse'></div>
</div>
</div>
))}
</div>
);
}3. Mensaje con progreso basado en useTransition (opcional)
Si bien loading.tsx no tiene acceso a hooks de React como useState o useEffect (porque es un componente que se renderiza en suspense), podés usar la API de next/navigation para leer el estado de la navegación mediante usePathname y useRouter, pero para indicadores de progreso más precisos suele ser mejor usar un loading.tsx estático y dejar que la UI de la página muestre su propio estado interno una vez que se renderiza.
Relación entre loading.tsx y error.tsx
Next.js también provee el archivo error.tsx para manejar errores inesperados en un segmento. Cuando una promesa lanza una excepción (o lanzás manualmente con throw new Error(...)), el framework busca el error.tsx más cercano y lo muestra en lugar del loading.tsx.
Ejemplo de error
app/dashboard/page.tsx
export default async function Page() {
await new Promise(res => setTimeout(res, 1000));
throw new Error('Falló la carga del panel');
return <p>Nunca se llega aquí.</p>;
}app/dashboard/error.tsx
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
return (
<div className='p-6 text-center text-red-600'>
<h2>Algo salió mal</h2>
<p>{error.message}</p>
<button onClick={reset} className='mt-4 px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600'>
Intentar de nuevo
</button>
</div>
);
}Qué se ve
- Al entrar a
/dashboard, se muestra elloading.tsx(si existe). - Después de 1 segundo, la promesa lanza el error.
- Next.js oculta el loading y muestra el
error.tsx. - El botón "Intentar de nuevo" llama a
reset(), lo que vuelve a intentar renderizar el segmento, volviendo a mostrar el loading mientras se reintenta la carga.
Salida esperada (comentada):
// t=0ms → loading (si existe)
// t=1000ms → error.tsx con mensaje y botón
// al hacer click en reset → vuelve a loading, luego otro error si la falla persisteBuenas prácticas
- Mantene el loading liviano: evité hacer cálculos pesados o consultas adicionales dentro de
loading.tsx, porque se renderiza cada vez que hay una suspense. - Usá clases de utilidad (Tailwind, CSS Modules) para evitar estilos que choquen con la página.
- No pongáis lógica de datos en loading: su único propósito es mostrar una UI de espera; los datos deben obtenerse en los componentes de página o layout.
- Aproveché el anidamiento: defini loadings específicos para segmentos que realmente los necesiten; así evitás mostrar un loading genérico cuando solo una subsección está lenta.
- Combiná loading y error: siempre que tengas un segmento que pueda fallar, proveé un
error.tsxpara ofrecer una experiencia de recuperación. - Evité el uso de
useEffectouseStatedentro de loading.tsx: no van a funcionar como esperás porque el componente se renderiza en modo suspense y se descarta tan pronto se resuelve la promesa. - Si necesitás indicadores de progreso más detallados, considerá usar una barra de progreso en el
layout.tsxque lea el estado de la ruta medianteusePathnamey el evento de navegación de Next.js.
Errores comunes
- Olvidar exportar el componente por defecto: Next.js espera un export default; si lo olvidás, verás un error en tiempo de desarrollo.
- Poner contenido que depende de datos asíncronos (por ejemplo, llamar a
fetchdentro de loading y esperar su resultado). Esto crea una nueva suspense y puede generar un bucle. - Usar estilos que sobrescriban accidentalmente el layout principal, haciendo que la página se vea distinta después del loading.
- Crear un loading.tsx en la raíz
app/y esperar que se muestre para todas las rutas: funciona, pero si tenés un layout que ya muestra su propio contenido mientras carga, el loading raíz puede nunca verse. - Confundir el comportamiento con
getStaticPropsogetServerSideProps: esos métodos pertenecen al Pages Router y no disparan Suspense automáticamente en el App Router.
Conclusión
El archivo loading.tsx es una pieza clave del App Router de Next.js que permite aprovechar el poder de Suspense de React sin escribir código extra. Al colocarlo estratégicamente en tu estructura de carpetas, conseguís:
- Una UI de carga automática que se muestra mientras los datos de un segmento se están obteniendo.
- La posibilidad de anidar loadings para reflejar con precisión qué parte de la interfaz está esperando.
- Una separación clara entre la lógica de carga (en
page.tsxolayout.tsx) y la presentación de espera (enloading.tsx).
Recordá siempre mantener el loading simple, usar el anidamiento cuando sea necesario y acompañarlo de un error.tsx para manejar fallos de forma elegante.
Con los ejemplos provistos, podés copiar y pegar los fragmentos en tu proyecto y ver de inmediato cómo se comporta la UI de carga. Ahora estás listo para construir aplicaciones Next.js que se sientan instantáneas y profesionales, incluso cuando los datos tarden en llegar.
Próximos pasos
- Experimentá con diferentes tipos de skeletons y animaciones.
- Explorá el uso de
loading.tsxjunto conroute.ts(los nuevos endpoints de la App Router) para manejar datos de APIs externas. - Revisá la documentación oficial de Next.js sobre Routing Fundamentals: Loading UI para profundizar en los detalles.
¡Éxitos codificando! 🚀
DUGLAS MORENO
Comentarios (0)
Sé el primero en comentar.