Entendé loading.tsx en Next.js App Router: UI de carga automática con Suspense

DUGLAS MORENODUGLAS MORENO 👁 16

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.tsx y 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.tsx en una ruta anidada.
  • Cómo personalizar la UI de carga con spinners, esqueletos o cualquier otro diseño.
  • La relación entre loading.tsx y error.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/analytics

Cuando el usuario navega a /dashboard/analytics, Next.js renderiza:

  1. El layout.tsx de la raíz.
  2. El layout.tsx de /dashboard.
  3. El loading.tsx de /dashboard mientras el segmento analytics no haya terminado de cargar sus datos.
  4. Una vez que la promesa del segmento analytics se resuelve, el loading.tsx se reemplaza por el page.tsx de analytics.

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.tsx

app/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

  1. Al entrar a /dashboard/analytics, Next.js muestra primero el loading.tsx de dashboard porque el segmento dashboard aún no ha terminado de cargar su layout (aunque no tenga datos, el hecho de que haya un hijo pendiente activa el Suspense).
  2. Mientras tanto, el segmento analytics empieza a cargar su propia promesa (3 segundos).
  3. Como hay un loading.tsx más específico (el de analytics), Next.js reemplaza el loading del padre por el del hijo tan pronto como detecta que el hijo está en su fase de carga.
  4. Pasados 3 segundos, la promesa de analytics se resuelve y se muestra el contenido de page.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

  1. Al entrar a /dashboard, se muestra el loading.tsx (si existe).
  2. Después de 1 segundo, la promesa lanza el error.
  3. Next.js oculta el loading y muestra el error.tsx.
  4. 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 persiste

Buenas prácticas

  1. 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.
  2. Usá clases de utilidad (Tailwind, CSS Modules) para evitar estilos que choquen con la página.
  3. 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.
  4. 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.
  5. Combiná loading y error: siempre que tengas un segmento que pueda fallar, proveé un error.tsx para ofrecer una experiencia de recuperación.
  6. Evité el uso de useEffect o useState dentro 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.
  7. Si necesitás indicadores de progreso más detallados, considerá usar una barra de progreso en el layout.tsx que lea el estado de la ruta mediante usePathname y 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 fetch dentro 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 getStaticProps o getServerSideProps: 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.tsx o layout.tsx) y la presentación de espera (en loading.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.tsx junto con route.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! 🚀

Comentarios (0)

Sé el primero en comentar.

Dejá tu comentario