Next es una tecnología fantástica en lo que respecta a Server Side Rendering y le debe gran parte de su éxito a su sintaxis análoga a React. En los últimos años, incluso parecería que la fusión entre ambas tecnologías (y de las organizaciones que las mantienen y promueven) ha traído parte de la API de backend al core de React. Sin embargo, el caso contrario, componentes de cliente en aplicaciones SSR, parece no estar igualmente contemplado y documentado.

Un ejemplo

Elijamos un ejemplo sencillo para Next: una aplicación en la que se muestre un listado de noticias (común para todos los lectores) con la posibilidad de marcar algunas de ellas como “favoritas”, a elección del visitante.

Aplicación en la que se muestre un listado de noticias.

Un análisis inicial enfocado al Server Side Rendering nos dirá que es importante servir el texto de las noticias desde el servidor con las capacidades SSR de Next, mientras que mostrar cuáles son favoritas o no debe ser despriorizado y no aparecer en la página servida, sino pintado posteriormente en cliente. Es irrelevante para los buscadores qué noticias son las favoritas de cada usuario y, además, seguramente esta información no estará disponible para los mismos ni debe estarlo.

Componetización y responsabilidad

Una componentización de la aplicación podría ser algo como esto:

App > ArticlesList > ArticleCard > FavButton

Cada uno de estos componentes muestra algo de la noticia (una lista en el caso de ArticlesList y los detalles en el caso de ArticleCard), estos datos deben ser recogidos desde una llamada en uno de los componentes de más alto nivel (App/page o ArticlesList) y finalmente tendremos FavButton, que cumple el doble propósito de mostrar si la noticia es favorita para el usuario o no y da la posibilidad de marcarla como tal.
Todos estos componentes hasta ArticleCard contendrán información relevante sobre la noticia, mientras que FavButton solo contiene interacción relevante para el usuario, así que todos serán componentes de servidor, a excepción de FavButton.

App > ArticlesList > ArticleCard > FavButton

Propongamos una versión de FavButton que lee y almacena si una noticia es favorita guardando su id en localStorage del navegador. Podríamos empezar por algo así (el resto de componentes es trivial en esta aplicación).

components/FavButton.js

'use client'
import { useState } from 'react'

const FavButton = ({ articleId }) => {
 const favs = () => {
   return JSON.parse(localStorage.getItem('favs') || '[]')
 }

 const [isFav, setIsFav] = useState(() => favs().includes(articleId))

 return (
   <button
     onClick={() => {
       localStorage.setItem(
         'favs',
         JSON.stringify(
           isFav
             ? favs().filter(f => f !== articleId)
             : [...favs(), articleId],
         ),
       )
       setIsFav(!isFav)
     }}
   >
     {isFav ? '❤️' : '♡'}
   </button>
 )
}

export default FavButton

NOTA: mantendremos la implementación lo más sencilla posible por tratarse de un ejemplo, pero en un proyecto real sería más correcto abstraer el acceso a localStorage en un hook personalizado o mejor aún, en un hook de Zustand con persistencia.

Al respecto de la arquitectura, cabe resaltar cuánta funcionalidad e interactividad se descarga sobre un componente pequeño y que está al final de la cadena de pertenencias. En un proyecto de React normal tenderíamos a crear componentes “contenedor” más inteligentes y componentes de presentación más “tontos”, pero en el caso de Next esta cantidad de responsabilidad parece revertida. Esto se debe a que de esta manera podemos hacer que solo los últimos componentes de la cadena sean de cliente y que no se incluyan en el SSR. Pronto veremos que tenemos que hacer algunas modificaciones sobre estos componentes también para asegurar la compatibilidad con SSR.

Empiezan los problemas

Si ejecutamos la aplicación en modo desarrollo con npm run dev en este momento, veremos que funciona (aunque tal vez recibamos algún fallo o aviso poco descriptivo en consola). Sin embargo, con Next el proceso de desarrollo no termina aquí, ya que el objetivo es generar páginas estáticas con Node.

Si intentamos construir el proyecto según lo tenemos ahora con npm run build nos encontraremos con un error en la terminal y que no se llega a completar el proceso de generación: "Reference error: localStorage is not defined".

Reference error: Local storage is not defined.

Este error se reproduce con cualquier componente que acceda a la API de cliente, incluso si hemos marcado el componente con ‘use client’.

Una forma de sortearlo es con un “return” en cualquier método que use la API de cliente. En nuestro caso, podemos modificar FavButton así:

components/FavButton.js

'use client'
import { useState } from 'react'

const FavButton = ({ articleId }) => {
 const favs = () => {
   if (typeof window === 'undefined') return []
   return JSON.parse(localStorage.getItem('favs') || '[]')
 }

 const [isFav, setIsFav] = useState(() => favs().includes(articleId))

 return (
   <button
     onClick={() => {
       localStorage.setItem(
         'favs',
         JSON.stringify(
           isFav
             ? favs().filter(f => f !== articleId)
             : [...favs(), articleId],
         ),
       )
       setIsFav(!isFav)
     }}
   >
     {isFav ? '❤️' : '♡'}
   </button>
 )
}

export default FavButton

Así, el método devolverá un valor por defecto sin tener que consultar la API de navegador en entornos (Node) en los que no existe.

Con este cambio ya podemos construir la aplicación (npm run build) y ejecutar la versión estática, que será la que se publique en producción con npm run start.

Vemos incluso que la funcionalidad de hacer favorita una noticia funciona, pero si abrimos la consola del navegador, encontraremos errores de este tipo:

Uncaught error: Minified React Error.

El código de error puede variar entre 418, 423 y 425. Si consultamos los enlaces que nos proporcionan veremos que 418 se corresponde con Hydration failed because the initial UI does not match what was rendered on the server; 423 se corresponde con There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering; y 425 se corresponde con Text content does not match server-rendered HTML. Resumiendo, tres maneras diferentes de decirnos que la versión guardada en servidor no se corresponde con la versión vista en cliente y Next espera que sí lo sea.

Importaciones dinámicas

Para librarnos de este error tendremos que recurrir a la importación dinámica de Next, lo que nos permitirá evitar la importación del componente problemático cuando estemos construyendo la versión SSR de la aplicación.

En nuestro caso, el componente que importa FavButton es ArticleCard. Modifiquémoslo para que la importación se haga de forma dinámica.

components/ArticleCard.js

import dynamic from 'next/dynamic'

const FavButton = dynamic(() => import('components/FavButton'), { ssr: false })

const ArticleCard = ({ title, subtitle, id }) => {
 return (
   <div>
     <div>
       <FavButton articleId={id} />
     </div>
     <div>
       <h2>{title}</h2>
       {subtitle}
     </div>
   </div>
 )
}

export default ArticleCard

Como vemos en las primeras líneas del archivo, importamos primero la librería Dynamic de Next y luego la usamos para importar FavButton dinámicamente, evitando que se importe en modo SSR. Esto implica que el componente no se incluirá al hacer build de la aplicación, pero ya desde el principio, cuando planteamos la estrategia SSR de la aplicación, decidimos que este no tenía valor para los buscadores.

Si ahora volvemos a construir la aplicación y ejecutamos la versión de producción (npm run build y npm run start) veremos que ambos procesos se ejecutan sin errores. En el navegador la funcionalidad es correcta y sin errores en consola y si miramos el código de la página veremos que desde el servidor recibimos el texto de todas las noticias, pero no los corazones, tal y como contemplamos en la planificación.

Conclusiones

Next JS supone una tecnología fantástica para páginas en las que SSR es obligatorio, pero la parte dinámica es igualmente necesaria en cualquier aplicación web. Es necesario establecer una estrategia SSR para favorecer el descubrimiento de las páginas y también conocer las técnicas de importación dinámicas necesarias para los componentes exclusivos de cliente.

Cuéntanos qué te parece.

Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.

Suscríbete

Estamos comprometidos.

Tecnología, personas e impacto positivo.