tutoriales.com

Aprovechando la Carga de Datos en el Cliente con SWR en Next.js App Router ⚡

Este tutorial explora cómo implementar la carga de datos en el cliente utilizando SWR en aplicaciones Next.js con el App Router. Aprenderás a configurar SWR, manejar el estado de carga y error, y revalidar datos para construir interfaces de usuario altamente interactivas y eficientes.

Intermedio15 min de lectura7 views
Reportar error

Next.js con su App Router ha revolucionado la forma en que construimos aplicaciones web, ofreciendo potentes capacidades de renderizado en el servidor y estático. Sin embargo, hay escenarios donde la carga de datos en el cliente es no solo deseable sino necesaria para una experiencia de usuario fluida y reactiva. Aquí es donde SWR (Stale-While-Revalidate) brilla con luz propia, proporcionando una estrategia robusta para la gestión de datos en el lado del cliente, perfectamente integrable con el App Router.

¿Por qué Carga de Datos en el Cliente con SWR? 🧐

Si bien el App Router de Next.js favorece la carga de datos en el servidor a través de async/await en React Server Components, hay casos de uso específicos donde la carga en el cliente es la mejor opción:

  • Interacciones dinámicas y en tiempo real: Datos que cambian frecuentemente o que necesitan ser actualizados sin recargar la página completa (chats, feeds de notificaciones, dashboards interactivos).
  • Dependencia del usuario: Datos que dependen de la interacción del usuario (filtros, búsquedas, paginación) o de información que solo está disponible en el cliente (geolocalización).
  • Datos que no son críticos para el renderizado inicial: Información auxiliar que puede cargarse después de que la página principal esté visible, mejorando el First Contentful Paint (FCP).
  • Migración gradual: Integrar SWR en componentes existentes que ya manejan lógica de fetching en el cliente.

SWR es una librería de fetching de datos de React que proporciona un mecanismo ligero para obtener, almacenar en caché y revalidar datos. Su nombre, "Stale-While-Revalidate", describe su estrategia: muestra datos en caché (stale), solicita los datos más recientes en segundo plano y luego los actualiza.

💡 Consejo: Aunque este tutorial se centra en SWR, es fundamental entender cuándo usarlo. Para datos estáticos o que cambian poco y son esenciales para el SEO y el primer renderizado, las funciones de fetching del servidor de Next.js (como `fetch` directamente en RSCs) suelen ser la mejor opción.

Configuración Inicial de SWR en tu Proyecto Next.js ✨

Antes de sumergirnos en la implementación, necesitamos asegurarnos de que nuestro proyecto Next.js esté listo. Asumiremos que ya tienes un proyecto con el App Router configurado.

1. Instalación de SWR

Primero, instala la librería SWR en tu proyecto:

npm install swr
# o
yarn add swr
pnpm add swr

2. Creando el Proveedor de SWR (SWRProvider)

Para que SWR funcione correctamente en tu aplicación, especialmente en un entorno con componentes de cliente y servidor, es una buena práctica envolver tu aplicación con un SWRConfig o un proveedor personalizado. Esto te permite configurar opciones globales de SWR, como el fetcher por defecto.

Crea un nuevo archivo, por ejemplo, app/providers.tsx (o donde organices tus proveedores de cliente):

'use client'; // Esto marca el archivo como un componente de cliente

import { SWRConfig } from 'swr';

interface SWRProviderProps {
  children: React.ReactNode;
}

export default function SWRProvider({ children }: SWRProviderProps) {
  const fetcher = (url: string) => fetch(url).then((res) => res.json());

  return (
    <SWRConfig value={{ fetcher }}>
      {children}
    </SWRConfig>
  );
}
🔥 Importante: La directiva `'use client'` es crucial. SWR interactúa con hooks de React (`useState`, `useEffect`), lo que significa que debe ejecutarse en el lado del cliente. Un `SWRProvider` global debe ser un Client Component.

3. Integrando el Proveedor en el Layout Principal

Ahora, envuelve tu RootLayout con este proveedor. El RootLayout (app/layout.tsx) es por defecto un Server Component, pero podemos anidar Client Components dentro de él.

// app/layout.tsx
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import './globals.css';
import SWRProvider from './providers'; // Importa el SWRProvider

const inter = Inter({ subsets: ['latin'] });

export const metadata: Metadata = {
  title: 'Next.js SWR Client Fetching',
  description: 'Aprende a usar SWR para la carga de datos en el cliente con Next.js App Router',
};

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="es">
      <body className={inter.className}>
        <SWRProvider> {/* Envuelve con SWRProvider */}
          {children}
        </SWRProvider>
      </body>
    </html>
  );
}

Con esto, SWR está configurado globalmente para usar un fetcher que simplemente realiza una petición fetch y parsea el JSON.


Implementando la Carga de Datos con useSWR 🛠️

Ahora que SWR está configurado, podemos empezar a usar el hook useSWR en nuestros Client Components.

1. Un Componente Básico de Carga de Datos

Vamos a crear un componente sencillo que carga una lista de publicaciones desde una API pública.

Crea un nuevo archivo, por ejemplo, app/components/PostsList.tsx:

'use client';

import useSWR from 'swr';

interface Post {
  id: number;
  title: string;
  body: string;
}

export default function PostsList() {
  // useSWR recibe la clave única para el dato y, opcionalmente, un fetcher (si no se define globalmente)
  const { data, error, isLoading } = useSWR<Post[]>('https://jsonplaceholder.typicode.com/posts');

  if (isLoading) return <p>Cargando publicaciones...</p>;
  if (error) return <p>Error al cargar: {error.message}</p>;
  if (!data) return <p>No hay publicaciones disponibles.</p>;

  return (
    <div className="posts-container">
      <h2>🚀 Mis Publicaciones Recientes</h2>
      <ul>
        {data.map((post) => (
          <li key={post.id}>
            <h3>{post.title}</h3>
            <p>{post.body.substring(0, 100)}...</p>
          </li>
        ))}
      </ul>
    </div>
  );
}

Explicación:

  • useSWR<Post[]>('https://jsonplaceholder.typicode.com/posts'): El primer argumento es la clave para identificar los datos. Puede ser una URL de API, una string o incluso un array. SWR usará esta clave para el cachéo y la revalidación. El tipo Post[] ayuda a TypeScript a inferir la forma de los datos.
  • data: Los datos obtenidos de la API, una vez que la carga es exitosa.
  • error: Un objeto Error si la petición falla.
  • isLoading: Un booleano que es true mientras la petición está en curso.

2. Integrando PostsList en una Página

Ahora, podemos usar este PostsList en cualquier página o componente de cliente de nuestra aplicación. Por ejemplo, en app/page.tsx:

// app/page.tsx

import PostsList from './components/PostsList';

export default function HomePage() {
  return (
    <main>
      <h1>Bienvenido a mi Aplicación Next.js</h1>
      <p>Aquí tienes algunos datos cargados en el cliente:</p>
      <PostsList />
    </main>
  );
}

Con esto, cuando navegues a la página principal, PostsList se renderizará en el cliente, mostrando un estado de carga mientras obtiene los datos, y luego las publicaciones una vez que lleguen.

📌 Nota: Al envolver `PostsList` en la página principal, que es un Server Component por defecto, Next.js se encarga de que `PostsList` se hydrate en el cliente y ejecute su lógica de fetching allí.

Opciones Avanzadas de SWR ⚙️

SWR ofrece una gran cantidad de opciones para personalizar el comportamiento del fetching, el cachéo y la revalidación.

1. Revalidación Condicional y a Intervalos

Por defecto, SWR revalida los datos cuando la ventana se enfoca (revalidateOnFocus) o cuando el dispositivo recupera la conexión de red (revalidateOnReconnect). Puedes configurar esto globalmente en el SWRProvider o localmente en el hook useSWR.

// app/components/DynamicData.tsx
'use client';

import useSWR from 'swr';

export default function DynamicData() {
  const { data, error, isLoading } = useSWR(
    '/api/realtime-data', // Clave de la API (asumiendo que tienes una API local)
    {
      refreshInterval: 5000, // Revalidar cada 5 segundos
      revalidateOnFocus: false, // Desactivar revalidación al enfocar la ventana
      revalidateOnReconnect: true, // Mantener revalidación al reconectar
    }
  );

  if (isLoading) return <p>Cargando datos dinámicos...</p>;
  if (error) return <p>Error: {error.message}</p>;

  return (
    <div style={{ padding: '20px', border: '1px solid #ccc' }}>
      <h3>📊 Datos Actualizados Cada 5 Segundos</h3>
      <p>Última actualización: <strong>{data?.timestamp || 'N/A'}</strong></p>
      <p>Valor: {data?.value || 'Cargando...'}</p>
    </div>
  );
}
⚠️ Advertencia: Un `refreshInterval` muy bajo puede sobrecargar tu servidor si no está diseñado para manejar muchas peticiones rápidas. Úsalo con moderación y para datos que realmente necesiten ser muy frescos.

2. Mutación de Datos (mutate) y Optimistic UI

SWR te permite mutar (mutate) los datos de la caché directamente, lo que es ideal para implementar una Interfaz de Usuario Optimista (Optimistic UI). Esto significa que puedes actualizar la UI inmediatamente después de una acción del usuario, y luego revalidar los datos con el servidor en segundo plano.

Imagina que tienes una API para marcar tareas como completadas:

// app/components/TodoItem.tsx
'use client';

import useSWR, { mutate } from 'swr';

interface Todo {
  id: number;
  title: string;
  completed: boolean;
}

export default function TodoItem({ todoId }: { todoId: number }) {
  const { data: todo, error } = useSWR<Todo>(`/api/todos/${todoId}`);

  if (error) return <p>Error al cargar la tarea.</p>;
  if (!todo) return <p>Cargando tarea...</p>;

  const toggleCompletion = async () => {
    // Actualiza la caché localmente de forma optimista
    // Esto hace que la UI se actualice instantáneamente
    mutate(`/api/todos/${todoId}`, { ...todo, completed: !todo.completed }, false); // `false` para no revalidar inmediatamente

    try {
      // Envía la petición al servidor
      await fetch(`/api/todos/${todoId}`, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ completed: !todo.completed }),
      });
      // Si todo va bien, SWR revalidará automáticamente si `revalidate: true` (por defecto) o puedes hacerlo manualmente
      mutate(`/api/todos/${todoId}`); // Revalidar para asegurar la consistencia con el servidor
    } catch (e) {
      console.error('Error al actualizar la tarea:', e);
      // En caso de error, puedes revertir la UI o mostrar un mensaje de error
      // mutate(`/api/todos/${todoId}`); // Revertir a los datos anteriores o forzar revalidación para mostrar el estado real
    }
  };

  return (
    <div style={{ display: 'flex', alignItems: 'center', marginBottom: '10px' }}>
      <input
        type="checkbox"
        checked={todo.completed}
        onChange={toggleCompletion}
        style={{ marginRight: '10px' }}
      />
      <span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
        {todo.title}
      </span>
    </div>
  );
}

3. Fetching con Argumentos y Dependencias

La clave de SWR no tiene por qué ser solo una string. Puede ser una función o un array, lo que te permite pasar argumentos al fetcher o hacer fetching condicionalmente.

// app/components/UserProfile.tsx
'use client';

import useSWR from 'swr';

interface UserProfileData {
  id: number;
  name: string;
  email: string;
}

// Un fetcher personalizado que acepta un ID de usuario
const userFetcher = (url: string, userId: number) =>
  fetch(`${url}/${userId}`).then((res) => res.json());

export default function UserProfile({ userId }: { userId: number | null }) {
  // La clave ahora es un array [URL, userId]
  const { data: user, error, isLoading } = useSWR<UserProfileData>(
    userId ? ['/api/users', userId] : null, // El fetching solo se activa si userId no es null
    ([url, id]) => userFetcher(url, id) // Nuestro fetcher personalizado
  );

  if (!userId) return <p>Selecciona un usuario para ver su perfil.</p>;
  if (isLoading) return <p>Cargando perfil del usuario...</p>;
  if (error) return <p>Error al cargar el perfil: {error.message}</p>;
  if (!user) return <p>Perfil no encontrado.</p>;

  return (
    <div style={{ border: '1px solid #ddd', padding: '15px', marginTop: '20px' }}>
      <h3>👤 Perfil de {user.name}</h3>
      <p>Email: {user.email}</p>
      <p>ID: {user.id}</p>
    </div>
  );
}

En este ejemplo, el fetching solo se activa cuando userId tiene un valor. Esto es útil para fetching condicional o cuando los datos dependen de estados o props.


Manejo de Errores y Carga Avanzado ⚠️

SWR ofrece herramientas robustas para gestionar los estados de error y carga, cruciales para una buena UX.

1. Reintentos y Revalidación en Caso de Error

Por defecto, SWR tiene una lógica de reintentos incorporada. Puedes personalizarla:

// app/components/ReliableData.tsx
'use client';

import useSWR from 'swr';

export default function ReliableData() {
  const { data, error, isLoading, isValidating } = useSWR(
    '/api/flaky-data', // Una API que podría fallar ocasionalmente
    {
      revalidateOnFocus: false, // No revalidar al enfocar
      revalidateOnReconnect: false, // No revalidar al reconectar
      errorRetryInterval: 3000, // Reintentar cada 3 segundos en caso de error
      errorRetryCount: 5, // Reintentar un máximo de 5 veces
    }
  );

  if (isLoading) return <p>Cargando datos (intentando)...</p>;
  if (error) return (
    <div className="callout warning">
      ⚠️ <strong>Error persistente:</strong> No se pudo cargar los datos después de varios intentos. <br/>
      Mensaje: {error.message}
    </div>
  );

  return (
    <div>
      <h3>✅ Datos Cargados Fiablemente</h3>
      <p>Últimos datos: {data?.value}</p>
      {isValidating && <p style={{ fontStyle: 'italic' }}>Revalidando en segundo plano...</p>}
    </div>
  );
}

isValidating es útil para mostrar un indicador de carga sutil cuando los datos se están revalidando en segundo plano (por ejemplo, después de una mutación o al enfocar la ventana), sin bloquear la UI con un spinner completo si los datos stale ya están disponibles.

2. Cachéo Local y Global

SWR mantiene una caché en memoria para tus datos. Cuando usas useSWR con la misma clave en múltiples componentes, todos compartirán los mismos datos cacheados. Esto evita peticiones redundantes y asegura la consistencia de los datos en toda tu aplicación.

Componente A useSWR('key') Componente B useSWR('key') SWR Global Cache ['key']: { data, timestamp } API / Fetcher Servidor Externo Datos Stale Instantáneo Revalidar Datos Nuevos Actualización Silenciosa

3. Prefetching con preload

SWR también ofrece una función preload para cargar datos antes de que un componente los necesite, mejorando la percepción de rendimiento. Esto es útil para rutas que sabes que el usuario probablemente visitará.

// app/components/ProductCard.tsx
'use client';

import useSWR, { preload } from 'swr';
import Link from 'next/link';

interface Product {
  id: number;
  name: string;
  description: string;
  price: number;
}

// Prefetch los datos del producto cuando el componente se monta
// (o en cualquier momento que quieras precargar)
preload('/api/products/123', (url) => fetch(url).then(res => res.json()));

export default function ProductCard({ productId }: { productId: number }) {
  const { data: product, error, isLoading } = useSWR<Product>(`/api/products/${productId}`);

  if (isLoading) return <p>Cargando producto...</p>;
  if (error) return <p>Error: {error.message}</p>;
  if (!product) return <p>Producto no encontrado.</p>;

  return (
    <div className="product-card">
      <h3>{product.name}</h3>
      <p>{product.description.substring(0, 150)}...</p>
      <p><strong>Precio: ${product.price.toFixed(2)}</strong></p>
      <Link href={`/products/${productId}`}>Ver Detalles</Link>
    </div>
  );
}

En este ejemplo, preload iniciará la carga del producto con ID 123 tan pronto como ProductCard se renderice (o incluso antes, si lo llamas en otro lugar). Si el usuario navega a la página de detalles de ese producto, SWR ya tendrá los datos o estará en proceso de obtenerlos, reduciendo el tiempo de espera.


Consideraciones y Mejores Prácticas con SWR y Next.js 🎯

Al integrar SWR en tu aplicación Next.js, ten en cuenta las siguientes recomendaciones:

  • Usa use client apropiadamente: Recuerda que SWR es un hook de React y, por lo tanto, solo puede usarse en Client Components. Coloca la directiva 'use client' al principio de tus archivos de componentes que usen useSWR.

  • Combinar RSC y Client Components: Aprovecha la fortaleza de Next.js. Usa Server Components para el renderizado inicial de la UI y los datos que son esenciales para el SEO o el First Contentful Paint. Luego, hidrata Client Components con SWR para funcionalidades dinámicas.

    Paso 1: RSCs obtienen datos estáticos/iniciales.
    Paso 2: HTML se envía al cliente rápidamente.
    Paso 3: CCs se hidratan y usan SWR para datos dinámicos/interactivos.
    Paso 4: La UI se actualiza de forma fluida con SWR.
  • Global Fetcher vs. Local Fetcher: Para la mayoría de los casos, un fetcher global en tu SWRProvider es suficiente. Sin embargo, si necesitas fetchers específicos para diferentes APIs o con lógica de autenticación particular, puedes pasarlos directamente al hook useSWR.

  • Manejo de estados: Siempre maneja los estados isLoading, error y data para proporcionar una experiencia de usuario robusta. Mostrar esqueletos de carga (skeletons) o mensajes de error claros es fundamental.

  • Estrategias de cachéo: SWR por defecto maneja un cachéo en memoria. Para una persistencia más allá del ciclo de vida de la sesión (por ejemplo, para guardar tokens de autenticación), considera soluciones como localStorage o IndexedDB, pero no directamente con SWR.

  • Intercepción de peticiones (middleware): Para añadir headers de autenticación, por ejemplo, puedes modificar tu fetcher global o crear un wrapper alrededor de fetch.

    // app/providers.tsx
    'use client';
    import { SWRConfig } from 'swr';
    
    const authenticatedFetcher = async (url: string) => {
      const token = localStorage.getItem('authToken'); // Obtener token del almacenamiento local
      const res = await fetch(url, {
        headers: {
          Authorization: token ? `Bearer ${token}` : '',
          'Content-Type': 'application/json',
        },
      });
      if (!res.ok) {
        // Manejar errores de autenticación o de la API
        const error = new Error('An error occurred while fetching the data.');
        // Attach extra info to the error object.
        // @ts-ignore
        error.info = await res.json();
        // @ts-ignore
        error.status = res.status;
        throw error;
      }
      return res.json();
    };
    
    export default function SWRProvider({ children }: { children: React.ReactNode }) {
      return (
        <SWRConfig value={{ fetcher: authenticatedFetcher }}>
          {children}
        </SWRConfig>
      );
    }
    
  • Pruebas: Al probar componentes que usan SWR, puedes mockear el hook useSWR para controlar los datos que devuelve, el estado de carga y los errores, haciendo que tus pruebas sean más predecibles y rápidas.


Conclusión 🎉

SWR es una herramienta increíblemente potente y flexible para manejar la carga de datos en el cliente en tus aplicaciones Next.js con el App Router. Al entender cuándo y cómo usarlo, puedes construir interfaces de usuario altamente reactivas, eficientes y con una excelente experiencia de usuario.

Combinando las capacidades de renderizado en el servidor de Next.js con la gestión de estado de datos en el cliente de SWR, obtendrás lo mejor de ambos mundos: un primer renderizado rápido y amigable para SEO, junto con una interactividad y dinamismo impecables para el usuario. ¡Ahora tienes las herramientas para llevar tus aplicaciones Next.js al siguiente nivel!

Tutoriales relacionados

Comentarios (0)

Aún no hay comentarios. ¡Sé el primero!