¿Por qué aprender Next.js?

¿Qué ventajas tiene Next.js? Server Side Rendering con Next ¿Cómo se usa getServerSideProps? Static Site Generation ¿Cómo se utiliza getStaticProps? ¿Cómo puedo generar una página estática sin hacer un deploy? Te contamos como puedes emplear Next.js junto con nuestra librería favorita, React, para crear aplicaciones más robustas y mejorar la experiencia de usuario.

Miguel Segura

Miguel Segura

¿Por qué Next.js?

En el post de la semana pasada estuvimos repasando las soluciones que empleamos para lidiar con los problemas de experiencia de usuario y de SEO de las single page aplications (SPA): el Server Side Rendering (SSR) y el Static Site Generation (SSG).

Hoy vamos a explorar como lucen estos conceptos en codigo usando React y Next.js, mientras te enseñamos a utilizar las últimas características de este framework como lo es el Incremental Static Regeneration (ISR) y el On-Demand Revalidatión.

Si quieres saber más sobre las novedades en React, puedes revisar "Lo nuevo de React 18" o puedes aprender a crear un React desde 0 con JavaScript para que comprendas su funcionamiento interno en este curso.


React, Vue, Angular y similares fueron creados con el propósito de permitirnos crear aplicaciones complejas fácilmente, sin embargo, por sí mismas pueden ser lentas durante la carga inicial y dañar el SEO de tu página web al mandar un HTML vacío, recuerda que este es sumamente importante para que los buscadores lleven tráfico a tu sitio de manera orgánica.

En este post iremos iterando la misma páginas con las distintas estrategias para que puedas apreciar su funcionamiento y aprender a implementarlas en Next.js. Estamos dramatizando el tiempo de carga debido a que es una aplicación sumamente pequeña. Todo el codigo de la pagina se encuentra en este repo.

Ver la página que simula una Single Page Aplication (SPA).

Como puedes observar hay un pantallazo con la página completamente vacía mientras pedimos datos al servidor y producimos la interfaz.

En React, traer datos del servidor y renderizarlos funciona de la siguiente manera:

export default function Pokemon() {
  const [data, setData] = useState({})

  const getPokemon = async () => {
    const { name, location_area_encounters, sprites } = await fetcher('https://pokeapi.co/api/v2/pokemon/1')
    const pokemonBattles = await fetcher(location_area_encounters)
    // Guardamos la informacion en el estado.
    setData({
      name,
      image: sprites.front_default,
      battles: pokemonBattles,
    })
  }

  // Tan pronto como podemos, pedimos la información necesaria a nuestra API.
  useEffect(() => {
    getPokemon()
  }, []);

  // Inicialmente no tenemos informacion, por lo que no renderizamos nada.
  if (!data.name) return null

  // Renderizamos un lindo pokemon.
  return (
    <div className="card">
      <div className="cardImage">
        <Image src={data.image} alt="Leonidas Esteban Logo" width={300} height={300} layout="responsive" />
      </div>
      <div>
        <h1>{data.name}</h1>
        <h2>Battle Locations:</h2>
        <ul>
          {data.battles.map(({ location_area: { name } }) => (
            <li key={name}>{name}</li>
          ))}
        </ul>
      </div>
    </div>
  )
}

getServerSideProps

Este es un ejemplo donde se utiliza el Server Side Render.

El tiempo de carga puede mejorar muchísimo si usamos esta estrategia en la que el contenido se renderiza en el servidor al momento en el que solicitamos la página y recibimos el contenido listo para mostrarlo en el navegador.

Debes emplear getServerSideProps solo si necesitas una página cuyos datos deben obtenerse en el momento de la solicitud, si nuestros datos cambian frecuentemente o si necesitamos páginas altamente personalizadas para el usuario.

Ver la página creada con getServerSideProps.

Como puedes ver estamos añadiendo la IP del usuario para denotar que la estamos personalizando en el servidor. En un entorno real, la página variaría según si el usuario está autenticado o no y los datos del mismo.

const fetcher = (url) => fetch(url).then((res) => res.json())

export default function Pokemon({ data, ip }) {
  return (
    <div className="container">
      <Head />

      <main className="main">
        <SideContent ip={ip} />
        <div className="card">
          <div className="cardImage">
            <Image src={data.image} alt="Leonidas Esteban Logo" width={300} height={300} layout="responsive" />
          </div>
          <div>
            <h2>{data.name}</h2>
            <h3>Battle Locations:</h3>
            <ul>
              {data.battles.map(({ location_area: { name } }) => (
                <li key={name}>{name}</li>
              ))}
            </ul>
          </div>
        </div>
      </main>

      <Footer />
    </div>
  )
}

export async function getServerSideProps({ req, res, query }) {
  // Traemos la data que vamos a necesitar, podemos utilizar los objetos req o query
  // para modificar el contenido segun la ruta o segun el usuario,
  const { name, location_area_encounters, sprites } = await fetcher('https://pokeapi.co/api/v2/pokemon/1')
  const pokemonBattles = await fetcher(location_area_encounters)

  const ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress

  return {
    props: {
      data: {
        name,
        image: sprites.front_default,
        battles: pokemonBattles,
      },
      ip,
    },
  }
}

getStaticProps

Esta es la función de Next.js que nos permite utilizar el Static Site Generation (SSG).

Al usar getStaticProps, Next.js ejecuta esta función al momento de realizar el build de producción de tu aplicación para generar el HTML necesario, de esta manera el tiempo de respuesta de la pagina sera extremadamente rápida y con un gran posicionamiento SEO.

Al ser una función que se ejecuta en el servidor al momento de ejecutar un proceso de deploy, esta no tiene acceso a la solicitud, a los query params o a los Headers HTTP de la solicitud.

Si los necesitas puedes utilizar Edge Functions

Si navegas dentro del sitio y prestas atención al tiempo de carga, esta estrategia será la más rápida.

Ver la página generada usando Static Site Generation

const fetcher = (url) => fetch(url).then((res) => res.json())

export default function Pokemon({ data }) {
  return (
    <div className="container">
      <Head />

      <main className="main">
        <SideContent />
        <div className="card">
          <div className="cardImage">
            <Image src={data.image} alt="Leonidas Esteban Logo" width={300} height={300} layout="responsive" />
          </div>
          <div>
            <h2>{data.name}</h2>
            <h3>Battle Locations:</h3>
            <ul>
              {data.battles.map(({ location_area: { name } }) => (
                <li key={name}>{name}</li>
              ))}
            </ul>
          </div>
        </div>
      </main>

      <Footer />
    </div>
  )
}

export async function getStaticProps() {
  // Traemos toda la informacion necesaria para renderizar una pagina web.
  const { name, location_area_encounters, sprites } = await fetcher('https://pokeapi.co/api/v2/pokemon/1')
  const pokemonBattles = await fetcher(location_area_encounters)

  return {
    props: {
      data: {
        name,
        image: sprites.front_default,
        battles: pokemonBattles,
      },
    },
  }
}

getStaticProps y getStaticPaths

Muchas veces necesitamos páginas estáticas únicas como por ejemplo: /pokedex o /terminos.

Sin embargo, muchas veces también necesitamos crear docenas de páginas con algunos cambios según el producto que muestran pero manteniendo la misma estructura:

En nuestro sitio, las utilizamos así:

/cursos /cursos/nombre-del-curso

Para hacer esto posible, Next.js nos permite utilizar rutas dinámicas simplemente añadiendo corchetes al nombre de una página: [parametro-dinamico], de esa manera el archivo que crees dentro de /pages/productos/[product-name].js se utilizara para generar de manera estática cada una de las páginas deseadas con información dinámica.

// Nuestro componente pokemon permancera exactmente igual que en el ejemplo anterior, sin embargo añadiremos las siguientes funciones dentro de /pokemones/[pokemon_id].js

export async function getStaticPaths() {
  const { results } = await fetcher('https://pokeapi.co/api/v2/pokemon/')
  const paths = results.map(({ url }) => ({ params: { pokemon_id: url.slice(34, -1) } }))

  //  Hemos creado un array con la siguiente estructura donde listamos
  // todos los posibles valores de los parametros de nuestra ruta
  // const paths = [
  //   { params: { pokemon_id: 1 } }
  //   { params: { pokemon_id: 2 } }
  //   { params: { pokemon_id: 3 } }
  // ]
  return {
    paths,
    fallback: false,
  };
}

export async function getStaticProps({ params: { pokemon_id } }) {
  // Traemos toda la informacion necesaria para renderizar una pagina web tomando en cuenta la ruta
  const { name, location_area_encounters, sprites } = await fetcher(`https://pokeapi.co/api/v2/pokemon/${pokemon_id}`)
  const pokemonBattles = await fetcher(location_area_encounters)

  return {
    props: {
      data: {
        name,
        image: sprites.front_default,
        battles: pokemonBattles,
      },
    },
  }
}

Con esto hemos logrado generar una serie de páginas estáticas, una por cada Pokémon de nuestra lista, con los datos de cada uno de ellos. Todo esto al momento de realizar el build de nuestra aplicación.

alt

Incremental Static Revalidation (ISR)

Ahora que ya sabemos como generar páginas estáticas a partir de la información de un producto o de un Pokémon en este caso, analicemos el siguiente problema:

Hemos generado una página estática para cada Pokémon con su nombre, su foto y las áreas donde ha tenido batallas. ¿Qué pasa si todos los días añadimos un nuevo Pokémon a la base de datos o tenemos que cambiar los datos de uno ya existente por alguna falta de ortografía? ¿Todos los días vamos a hacer un deploy para generar cambios en una página de los miles que podemos llegar a tener?

Este es un caso de uso común, nosotros agregamos un proyecto nuevo cada semana creando una nueva ruta tipo: /proyectos/slug y un ecommerce que utiliza Next.js añade productos regularmente creando rutas tipo: /productos/id-producto. Y para no tener que realizar deploy utilizamos:

  • fallback: Para crear nuevas rutas.

  • Incremental Static Revalidation: Para editar el contenido ocasionalmente.

Si fallback es true o blocking, Next.js va a crear las rutas generadas al momento de ejecutar el build, sin embargo, también tratará de crear nuevas bajo demanda

export async function getStaticProps({ params: { pokemon_id } }) {
  const { name, location_area_encounters, sprites } = await fetcher(`https://pokeapi.co/api/v2/pokemon/${pokemon_id}`)
  const pokemonBattles = await fetcher(location_area_encounters)

  return {
    props: {
      data: {
        name,
        image: sprites.front_default,
        battles: pokemonBattles,
      },
    },
    // Next.js intentara regenerar la pagina:
    // - Cuando llegue un nuevo request
    // - Por lo menos una vez cada 60 segundos
    revalidate: 60,
  }
}

export async function getStaticPaths() {
  const { results } = await fetcher('https://pokeapi.co/api/v2/pokemon/')
  const paths = results.map(({ url }) => ({ params: { pokemon_id: url.slice(34, -1) } }))
  return {
    paths,
    fallback: true,
    // fallback: blocking,
  };
}

¿Quieres probarlo?

Al momento de realizar el deploy de este proyecto, solo generamos las páginas de los 20 primeros pokemones, sin embargo, la API es capaz de retornarnos más de 1000, así que usando: Incremental Static Regeneration (ISR)` debemos de tener más de 1000 páginas estáticas veloces y optimizadas para el SEO.

Cuando navego a una ruta que no existe al momento del deploy como: /pokemones/820

Podemos ver que se ejecuta la función que se encarga de generar la página estática correspondiente, así esta estará disponible en segundos cuando un usuario la necesite.

alt

On-demand Revalidation

En la versión más reciente de Next.js nació esta característica que nos permite actualizar y producir páginas estáticas a través de la API cuando el contenido de una página ha cambiado.

Al crear o actualizar un elemento puedes llamar a la API desde tu Backend para mantener tu sitio actualizado y optimizado.

// pages/api/revalidate.js

export default async function handler(req, res) {
  // Crea algun tipo de autorizacion para la ruta
  if (req.query.secret !== process.env.MY_SECRET_TOKEN) {
    return res.status(401).json({ message: 'Invalid token' })
  }

  try {
    await res.revalidate('/ruta-a-revalidar')
    return res.json({ revalidated: true })
  } catch (err) {
    // Si ocurre un error, Next.js le seguira mostrando la ultima
    // Version de la pagina a los usuarios.
    return res.status(500).send('Error revalidating')
  }
}

Si te gusto este post, compártelo en tus redes sociales, cuéntanos que te gustaría continuar aprendiendo, te invito a dejarme un estrellita en el repositorio del mismo.

Ilustracion que representa como crece alguien profesionalmente

Entérate de las últimas novedades

Streamings, Noticias y Early Adopter bonus. Sé el primero en enterarte de todo.