Cómo obtener datos en Next.js

Cómo obtener datos en Next.js Créditos de imagen: Unsplash

Next.js es un framework de JavaScript que permite crear sitios web utilizando Node.js en el lado del servidor, y React.js en el lado del cliente. Está preparado tanto para crear páginas estáticas como para renderizar páginas en el servidor dinámicamente, pudiendo utilizar ambas tecnologías al unísono en un mismo sitio web.

En este artículo se tratará el asunto de la obtención de datos para agregarlos al pre-renderizado de páginas en el servidor. Dependiendo de la tecnología usada para este pre-renderizado, Next.js usará diferentes técnicas para obtener los datos:

  • Generación Estática, usando estas funciones:
    • getStaticProps: Obtener datos en tiempo de construcción.
    • getStaticPaths: Especificar rutas dinámicas para pre-renderizar páginas basadas en datos.
  • Renderizado Server-Side, usando esta función:
    • getServerSideProps: Obtener datos en cada consulta de la página.

Por otro lado, se puede obtener datos en el lado del cliente usando librerías como Axios, o combinar la obtención de datos en el servidor y el cliente usando SWR.

getStaticProps (Generación Estática)

Exportando la función asíncrona getStaticProps desde una página, Next.js pre-renderizará ésta en el momento de crearse usando los props devueltos por la función. Estos props devueltos deberá ser un objeto serializable, normalmente JSON.

Si los datos no se encuentran se podrá devolver un estado 404 (notFound: true) ó redirigir a otra página.

¡AVISO! Hay un problema usando getStaticProps junto a Link. Este componente carga las páginas en el cliente a modo de SPA usando ruteo en el navegador, por lo que no se trae las nuevas versiones del servidor. Para evitar esto es necesario usar la etiqueta <a> directamente, lo que traerá la página completa del servidor si el TTL de Caché-control ha expirado.

Regeneración Estática Incremental

Con esta característica de getStaticProps el contenido estático también puede ser dinámico, pues permite actualizar páginas estáticas existentes pre-renderizándolas en segundo plano a la vez que se atiende al tráfico sin interrupción.

Añadiendo la propiedad revalidate: 1 al objeto devuelto por getStaticProps, la página se actualizará cada segundo sin necesidad de reconstruir la aplicación entera.

Aquí se puede encontrar un ejemplo del funcionamiento de esta característica.

No se debería traer datos (fetch) de una ruta API de la aplicación desde getStaticProps. En vez de esto se debería escribir el código server-side directamente en getStaticProps, por ejemplo para acceder a una base de datos.

Ejemplo

export async function getStaticProps(context) {
  const res = await fetch(`https://.../data`)
  const data = await res.json()

  if (!data) {
    return {
      // notFound: true, // Devolverá la página 404
      redirect: {
        destination: '/',
        permanent: false,
      },
    }
  }

  return {
    props: { data }, // será pasado al componente Blog como data
    
    // *** Opción para Regeneración Estática Incremental ***
    // Next.js will attempt to re-generate the page:
    // - When a request comes in
    // - At most once every second
    revalidate: 1, // In seconds
  }
}

function Blog({ data }) {
  return (
    <ul>
      {data.map((post) => (
        <li>{post.title}</li>
      ))}
    </ul>
  )
}

export default Blog

¿Cuándo usar getStaticProps?

  • Los datos requeridos para renderizar la página están disponibles en tiempo de construcción antes del request del usuario.
  • Los datos vienen de un headless CMS.
  • Los datos pueden ser cacheados públicamente (independientes del usuario).
  • La página debe ser pre-renderizada (para SEO) y muy rápida.

getStaticPaths (Generación Estática)

Si una página tiene rutas dinámicas y usa getStaticProps, necesitará definir esa lista de rutas que deben ser renderizadas a HTML en tiempo de construcción. Next.js estáticamente pre-renderizará todas las rutas especificadas en getStaticPaths.

export async function getStaticPaths() {
  return {
    paths: [
      { params: { id: '1' } },
      { params: { id: '2' } }
    ],
    fallback: true // or false
  };
}

La propiedad paths (requerida)

La propiedad paths define las rutas que serán pre-renderizadas. Suponiendo que tienes una página que usa rutas dinámicas llamada pages/posts/[id].js, si exportas getStaticPaths desde esa página y devuelves esos paths, Next.js estáticamente generará post/1 y post/2 en tiempo de construcción usando el componente de página en pages/posts/[id].js.

El valor de cada params debe coincidir con los parámetros usados en el nombre de la página. Si el nombre de la página es pages/posts/[postId]/[commentId], entonces params debe contener postId y commentId.

La propiedad fallback (requerida)

fallback: false

Si es falso, entonces cualquier ruta no devuelta por getStaticPaths resultará en la página 404 (no encontrada). Esto es conveniente si se tiene un número limitado de rutas para pre-renderizar. También es útil cuando no se crean nuevas páginas a menudo. En este caso cuando se añada nuevos ítems en la fuente de datos y se necesite renderizar nuevas páginas, se deberá construir la aplicación nuevamente.

El siguiente ejemplo pre-renderizará los post de un blog mediante la página pages/posts/[id].js, obteniendo la lista de post desde un CMS.

// pages/posts/[id].js

function Post({ post }) {
  // Render post...
}

// This function gets called at build time
export async function getStaticPaths() {
  // Call an external API endpoint to get posts
  const res = await fetch('https://.../posts')
  const posts = await res.json()

  // Get the paths we want to pre-render based on posts
  const paths = posts.map((post) => ({
    params: { id: post.id },
  }))

  // We'll pre-render only these paths at build time.
  // { fallback: false } means other routes should 404.
  return { 
    paths, 
    fallback: false 
  }
}

// This also gets called at build time
export async function getStaticProps({ params }) {
  // params contains the post `id`.
  // If the route is like /posts/1, then params.id is 1
  const res = await fetch(`https://.../posts/${params.id}`)
  const post = await res.json()

  // Pass post data to the page via props
  return { props: { post } }
}

export default Post

fallback: true

En este caso:

  • Las rutas devueltas por getStaticPaths serán renderizadas a HTML en tiempo de construcción por getStaticProps.
  • Las rutas que no se hayan generado en tiempo de construcción no resultarán en la página 404. En vez de esto, Next.js devolverá una versión fallback de la página en la primera llamada a esa ruta.
  • En segundo plano, Next.js generará estáticamente la ruta llamada a HTML y JSON. Esto incluye ejecutar getStaticProps.
  • Cuando ésto esté hecho, el navegador recibirá el JSON de la ruta generada. Esto será usado para automáticamente renderizar la página con los props requeridos. Desde la perspectiva del usuario, la página será cambiada de la versión fallback a la página completa.
  • Al mismo tiempo, Next.js añade esta ruta a la lista de páginas pre-renderizadas. Subsecuentes llamadas a la misma ruta devolverán la página generada, tal como se hace con las páginas pre-renderizadas en tiempo de construcción.

En la versión fallback de una página:

  • Los props de la página estarán vacíos.
  • Usando el router, se puede detectar si el fallback está siendo renderizado. router.isFallback será true.

Este es un ejemplo que usa router.isFallback:

// pages/posts/[id].js
import { useRouter } from 'next/router'

function Post({ post }) {
  const router = useRouter()

  // If the page is not yet generated, this will be displayed
  // initially until getStaticProps() finishes running
  if (router.isFallback) {
    return <div>Loading...</div>
  }

  // Render post...
}

// This function gets called at build time
export async function getStaticPaths() {
  return {
    // Only `/posts/1` and `/posts/2` are generated at build time
    paths: [
      { params: { id: '1' } }, 
      { params: { id: '2' } }
    ],
    // Enable statically generating additional pages
    // For example: `/posts/3`
    fallback: true,
  }
}

// This also gets called at build time
export async function getStaticProps({ params }) {
  // params contains the post `id`.
  // If the route is like /posts/1, then params.id is 1
  const res = await fetch(`https://.../posts/${params.id}`)
  const post = await res.json()

  // Pass post data to the page via props
  return {
    props: { post },
    // Re-generate the post at most once per second
    // if a request comes in
    revalidate: 1,
  }
}

export default Post

¿Cuándo es útil fallback: true?

Esta opción es útil cuando se tiene un número grande de páginas estáticas que dependen de datos (p.e. un sitio de comercio electrónico). Se quiere pre-renderizar todas las páginas de los productos, por lo que construir la aplicación completamente se haría eterno.

En vez de ésto, se puede generar estáticamente un pequeño grupo de páginas y usar fallback: true para el resto. Cuando alguien consulte una página que no está generada todavía, el usuario verá la página con el indicador de carga. Al poco rato getStaticProps finalizará y la página será renderizada con los datos requeridos, pudiendo a partir de ahora consultar la página pre-renderizada estáticamente.

De esta forma se consigue una experiencia de usuario rápida mientras se mantiene unos tiempos de construcción rápidos.

fallback: 'blocking'

En este caso las nuevas rutas esperarán a que el HTML sea generado, como ocurre en el renderizado server-side, y que sea cacheado para futuras consultas, lo que ocurre solamente una vez por ruta.

getStaticProps hará lo siguiente:

  • Las rutas devueltas por getStaticPaths serán renderizadas a HTML en tiempo de construcción por getStaticProps.
  • Las rutas que no se hayan generado en tiempo de construcción no resultarán en la página 404. En vez de esto, Next.js renderizará server-side en la primera consulta y devolverá el HTML generado.
  • Cuando ésto esté hecho, el navegador recibirá el HTML de la ruta generada. Desde la perspectiva del usuario, será una transición desde “el navegador está consultando la página” a “la página completa está cargada”. No hay aviso de estado de carga.
  • Al mismo tiempo, Next.js añade esta ruta a la lista de páginas pre-renderizadas. Subsecuentes llamadas a la misma ruta devolverán la página generada, tal como se hace con las páginas pre-renderizadas en tiempo de construcción.

Ni fallback: true ni fallback: 'blocking' actualizarán páginas generadas por defecto. Para actualizar páginas generadas se deberá usar regeneración estática incremental junto a estas opciones.

getServerSideProps (Renderizado en cada request al servidor)

Exportando la función asíncrona getServerSideProps desde una página, Next.js pre-renderizará ésta en cada request del cliente, usando los datos devueltos por getServerSideProps.

Si los datos no se encuentran se podrá devolver un estado 404 (notFound: true) ó redirigir a otra página.

Para cachear la página es necesario definir la propiedad context.res.setHeader(). De esta forma se pre-renderizará la página en cada request del cliente sólo si el TTL ha expirado (en el siguiente ejemplo ocurre cada hora).

function Page({ data }) {
  // Render data...
}

// This gets called on every request
export async function getServerSideProps(context) {
  // Fetch data from external API
  const res = await fetch(`https://.../data`)
  const data = await res.json()

  context.res.setHeader('Cache-Control', 'public, max-age=3600, must-revalidate');

  if (!data) {
    return {
      redirect: {
        // notFound: true, // Devolverá la página 404
        destination: '/',
        permanent: false,
      },
    }
  }

  // Pass data to the page via props
  return { props: { data } }
}

export default Page

¿Cuándo usar getServerSideProps?

  • Se debería usar sólo si se necesita pre-renderizar una página cuyos datos deben ser obtenidos en tiempo de request. El tiempo de carga será más lento que con getStaticProps porque el servidor deberá procesar el resultado en cada request, y el resultado no puede ser cacheado en un CDN sin configuración extra.

Si no se necesita pre-renderizar los datos, se debería considerar obtener los datos en el lado del cliente.

Obtener datos en el lado del Cliente

Si la página contiene datos que se actualizan frecuentemente, y no se necesitan pre-renderizar, se pueden obtener en el lado del cliente.

  1. Primero se muestra la página sin datos. Partes de la página se pueden pre-renderizar usando SSG. Se puede mostrar estados de carga mientras se espera a los datos.
  2. Después obtener los datos en el cliente y mostrarlos cuando estén disponibles.

Esta solución suele ser válida para paneles de control, pues éstos son privados y dependientes del usuario. SEO no es relevante y la página no necesita ser pre-renderizada. Los datos cambian frecuentemente, por lo que se requiere la obtención de datos en tiempo de request.

SWR

SWR es un hook de React para la obtención de datos. Este módulo gestiona características como la caché, el revalidado o la recuperación de datos por intervalos.

import useSWR from 'swr'

function Profile() {
  const { data, error } = useSWR('/api/user', fetch)

  if (error) return <div>failed to load</div>
  if (!data) return <div>loading...</div>
  return <div>hello {data.name}!</div>
}

Junto con SWR, se puede usar pre-renderizado SSG o SSR para SEO.

Se pasará los datos pre-obtenidos como valor inicial en la opción initialData. La página será pre-renderizada con esos datos, lo que significa que estará preparada para SEO, se podrá cachear y su acceso será muy rápido. A demás, al usar SWR en el lado del cliente, los datos serán actualizados dinámicamente mediante la interacción del usuario.

const fetcher = (...args) => fetch(...args).then(res => res.json());

export async function getStaticProps() {
  // `getStaticProps` is invoked on the server-side,
  // so this `fetcher` function will be executed on the server-side.
  const posts = await fetcher('/api/posts')
  return { props: { posts } }
}

function Posts (props) {
  // Here the `fetcher` function will be executed on the client-side.
  const { data } = useSWR('/api/posts', fetcher, { initialData: props.posts })

  // ...
}

En el ejemplo anterior, se usa fetcher para cargar los datos tanto en el cliente como en el servidor, por lo que necesita ser compatible con ambos entornos. Sin embargo, se puede usar diferentes formas para cargar los datos en el cliente o en el servidor.

Recursos

Artículos relacionados

Comentarios