Los componentes de servidor de React forman parte del core de la librería desde 2022, aunque el cambio al nuevo paradigma ha resultado largo y polémico desde el principio, tal vez por lo confuso de su presentación inicial como algo netamente ventajoso o “el siguiente nivel”.

Como vamos a ver en este artículo, los server components suponen una mejora en arquitectura y “estética” de código en el caso de las aplicaciones server side o más específicamente Next JS de Vercel, la compañía junto a la que se están definiendo e implementando ad hoc.

Next JS antes y después

Este es un componente sencillo de Next JS en el que se hace una petición a la API sin usar Server Components:

const Page = ({ data }) {
  <div>{data}</div>
}

const getServerSideProps = async () => {
  const res = await fetch(`https://.../data`)
  const data = await res.json()
  return { props: { data } }
}

export default Page
export { getServerSideProps }

A continuación, vemos el mismo ejemplo usando server components.

const Page = async () => {
  await  fetch(‘http://some-api.com’).then(res => res.json())
  return (<div>{data}</div>)
}

export default Page

Como ves, no hay ninguna mejora tecnológica ni de funcionalidad, pero el código queda mucho más estético para el desarrollador y nos libramos de ciertos métodos específicos del antiguo Next JS. Ni siquiera usamos métodos del core de React (un useEffect sería esperable en este caso) sino directamente los nativos propios de Javascript: async, await y fetch.

Por supuesto, estos componentes vienen renderizados desde el servidor, por lo que son más fácilmente indexables por herramientas de posicionamiento (robots de buscadores clásicamente, pero también otros nuevos como las IA’s).

El principal cambio (¡y bienvenido sea!) es que los métodos getStaticProps y getServerSideProps (exclusivos de Next JS) han quedado desfasados y ahora el resultado es más natural y legible. Veremos también cómo getStaticPaths y getStaticProps se sustituyen por generateStaticParams (exclusivo de Next JS), también con una sintaxis mejorada.

La controversia

El cambio hacia los server components resulta magro y estético de momento, pero queda lejos de resultar paradigmático. Esto es porque todavía no hemos entrado en el resto de novedades de Next JS como el nuevo enrutador App Router, que sustituye al clásico Pages Router, ni a la progresiva adaptación a la nueva API de React con Suspense.

Aquí es donde nos topamos con la polémica colaboración entre React y Vercel: Suspense, concurrent mode y otros, son componentes core de React que llevan años en definición o implementación experimental y prometían no nuevas funcionalidades, sino una abstracta ventaja en desarrollo que en su momento no fue bien explicada y que finalmente se han revelado como placeholders para integración con otras librerías o tecnologías, de momento y de forma más notoria, con Next JS. Siendo Vercel una empresa que, de hecho, ha absorbido parte de los desarrolladores de React con este objetivo.

Independientemente de la polémica que esta absorción parcial pueda provocar, los nuevos avances también nos hacen cuestionar el lugar de Next en el universo React.

Para empezar, Next JS ofrece una funcionalidad muy útil, ya que al basarse en Server Side Rendering, sus aplicaciones son más fácilmente indexables por los buscadores y esto es fundamental para cualquier proyecto comercial.

Por otro lado, una aplicación SSR no es interactiva/reactiva, no utiliza el ciclo de vida de los componentes de React, sino métodos propios, hasta el punto de cuestionar si es una aplicación de React. Más bien, se beneficia de la accesibilidad de JSX y de la popularidad de React, pero no pueden considerarse aplicaciones “reactivas”.

La implementación

Como siempre con Next JS, empezaremos por preguntarnos por qué estamos usándolo. La respuesta siempre es “porque queremos que nuestra aplicación sea más descubrible por los buscadores”.

Esto nos guía por una especie de ingeniería inversa que es innecesaria con las aplicaciones SPA (Single Page Application). Ya no pensamos en cuál es la forma más económica de gestionar los datos y la navegación, sino pensar en qué ve un robot al visitar nuestra aplicación, priorizando los datos que queremos que estén presentes en la página desde el principio y dejando en segundo lugar los que no son tan relevantes para el SEO, así como la interacción.

De alguna manera, los paradigmas de una SPA y de una aplicación SSR son casi opuestos.

La mejor manera de iniciar un proyecto Next es con create-next-app.

npx create-next-app@latest

Las nuevas versiones inician un diálogo desde el que podremos elegir el nombre de la aplicación y algunas características de la configuración. Como siempre, dejar las opciones por defecto suele ser una apuesta segura, pero puede que nuestras preferencias y costumbres sean diferentes.

La home

Una de las partes más importantes de la aplicación orientada a SEO es la home. Antes de empezar, debemos pensar qué partes queremos que sean visibles para los robots de búsqueda, cuáles queremos que se carguen más rápido…

Para nuestro ejemplo, pongamos que queremos una aplicación para un periódico y priorizaremos que las noticias del día vengan “hidratadas” desde el servidor.

Otras funciones, como log-in de usuario o publicidad, serán secundarias o incluso irrelevantes para los buscadores y se pueden relegar a un componente de cliente.

Dónde colocar nuestras nuevas páginas es una de las primeras diferencias con el antiguo Next: antes era en la carpeta pages mientras que ahora van en la carpeta app. Además, ahora cada ruta es una carpeta y la página page.js dentro de ella, la página que se visita.

En la nueva arquitectura de Next, tendremos componentes de servidor (cualquiera por defecto) y otros de cliente. Separarlos en carpetas (o no) está a nuestra elección; la distinción principal es que los componentes de cliente tienen acceso a hooks y van marcados con la frase ‘use client’ al principio (los de servidor no hace falta marcarlos).

En cuanto a patrones, la propia arquitectura favorece el de responsabilidad única por encima del de contenedores o componentes listos y componentes tontos. Siendo así, intentaremos que nuestros componentes haga una cosa y solo una.

// /src/app/page.js
import { getPosts } from 'api'
import { PostsListInteractive } from 'components/client'

const Page = async () => {
 const posts = await getPosts()

 return (
   <div>
     <PostsListInteractive posts={posts} />
   </div>
 )
}

export default Page
// /src/app/components/server/PostsList.js
import PostCard from './PostCard'

const PostList = ({ posts }) => (
 <ul>
   {posts.map(post => (
     <li key={post.id}>
       <PostCard {...post} />
     </li>
   ))}
 </ul>
)

export default PostList
// /src/app/components/server/PostCard.js
import Link from 'next/link'

const PostCard = ({ body, id, title }) => (
 <article>
   <header>
     <Link href={`/post/${id}`}>
       <h2>{title}</h2>
     </Link>
   </header>
   <div>
     {body}
   </div>
 </article>
)

export default PostCard

Todos estos componentes son componentes de servidor, ya que es la convención en Next JS a partir de la versión 13. Funciones como getPosts son simplemente métodos fetch abstraídos en su propio archivo de la siguiente manera (aunque el uso de una librería podría ser más apropiado en un proyecto para producción real).

// /src/app/api/getPosts.js
const getPost = ({ id }) =>
 fetch(`https://jsonplaceholder.typicode.com/posts/${id}`)
   .then(res => res.json())

export default getPost

Por supuesto, podemos tener una vista previa de la página (no definitiva como veremos en un momento) ejecutando el comando npm run dev. El resultado conseguido es una página home con unos cuantos posts que formarán el contenido de nuestra home.

Vista previa página

Al ser Next JS una tecnología orientada al servicio desde servidor, podemos comprobar el resultado que llega sin modificación en el cliente con la opción ver código de la página de nuestro navegador.

Resultado en la opción ver código de la página de nuestro navegador.

Resultado del código en el navegador

Efectivamente, todo el texto está ahí cuando la página es servida, en lugar de la típica ventana en blanco de una aplicación normal de React.

Mencionábamos antes la integración de Next JS con Suspense. Ahora la pondremos a prueba de una de las posibles maneras.

// /src/app/loading.js
const Loading = () => (
 <div>
   <h1>LOADING...</h1>
 </div>
)

export default Loading

Siguiendo esta convención, simplemente colocando un archivo loading.js en la misma carpeta que una página o componente, este quedará rodeado virtualmente de un componente Suspense que se sustituirá por este placeholder mientras se carga el contenido.

Posts individuales y rutas dinámicas

Nos ha quedado una home un poco sosa pero funcional, particularmente a nivel de SEO. Ahora intentaremos hacer lo propio con la página de posts individuales.

Por supuesto, pensaremos primero en que se sirva desde servidor el contenido y luego en mejoras de interactividad o contenidos secundarios.

En este caso, la noticia en sí es el asset más relevante y sus comentarios lo secundario, que podrá cargarse aleatoriamente a petición del cliente.

Para nuestro ejemplo querremos rutas de tipo /post/id donde el id es el identificador del post (en un ejemplo real, sería más descubrible utilizar abreviaturas del título en la url o slugs).

El método para registrar rutas dinámicas de este tipo es el mismo que con el antiguo enrutador de pages: en la carpeta app creamos una nueva con el nombre post, dentro de esta otra con el nombre [id] (ya que es la parte dinámica de la ruta) y dentro de esta última, un archivo page.js.

Una primera implementación podría ser así:

// /src/app/post/[id]/page.js
import { getAllPosts, getPost } from 'api'
import { PostCard } from 'components/server'
import { CommentsList } from 'components/server'

const Page = async ({ params: { id } }) => {
 const post = await getPost({ id })

 return (
   <main>
     <PostCard {...post} />
     <CommentsList postId={id} />
   </main>
 )
}

export default Page

Por supuesto, tendremos que implementar los componentes PostCard y CommentList por su cuenta, así como la llamada trivial getPost, pero esto no debería resultar muy difícil.

Una mejora rápida que podemos implementar es un componente loading específico para la lista de comentarios. Así el usuario sabrá qué es lo que se está cargando.

// /src/app/post/[id]/page.js
import { getAllPosts, getPost } from 'api'
import { PostCard } from 'components/server'
import { CommentsList } from 'components/server'

const Page = async ({ params: { id } }) => {
 const post = await getPost({ id })

 return (
   <main>
     <PostCard {...post} />
           <Suspense fallback={<h1>Loading comments...</h1>}>
       <CommentsList postId={id} />
    </Suspense>
   </main>
 )
}

export default Page

Más relevante, ya que afecta al SEO y a una experiencia de usuario más rápida, es asegurarnos de que las páginas de artículos existan ya en servidor de forma estática. Los comentarios también vendrán servidos desde el servidor (Server Side Rendering o SSR) pero de forma dinámica.

Los artículos, queremos que existan ya (Static Site Generation o SSG). En una misma página podemos combinar las dos formas de rendering. En el pasado, con el enrutador Pages, habríamos usado los métodos getStaticPaths y getStaticProps para señalar las rutas que queremos pre-renderizar.

Con el nuevo enrutador App, usaremos el método generateStaticParams que, desde la propia página, devuelve una lista con los posibles valores que puede tomar la parte dinámica de la ruta.

Para ello, tenemos que hacer una llamada a servidor que sólo se ejecutará en tiempo de pre-renderizado antes de publicar la aplicación.

// /src/app/post/[id]/page.js
import { Suspense } from 'react'
import { getAllPosts, getPost } from 'api'
import { PostCard } from 'components/server'
import { CommentsList } from 'components/server'

const Page = async ({ params: { id } }) => {
 const post = await getPost({ id })

 return (
   <main>
     <PostCard {...post} />
     <Suspense fallback={<h1>Loading comments...</h1>}>
       <CommentsList postId={id} />
     </Suspense>
   </main>
 )
}

const generateStaticParams = async () => {
 const posts = await getAllPosts()

 return posts.map(({ id }) => ({
   params: { id },
 }))
}

export default Page
export { generateStaticParams }

Un poco de interactividad

Tal y como era hasta ahora, la nueva arquitectura de Next nos permite orientar el renderizado de nuestras aplicaciones para servirlas desde el servidor directamente y hacerlas más descubribles. Pero, como toda aplicación, en algún momento querremos añadir interactividad.

En nuestro ejemplo, podemos probar a convertir los enlaces a las noticias de la home de manera que inicialmente muestren solo el título y, al darle a un botón, se abran para ver el subtítulo.

Es un ejemplo muy simple, pero nos servirá para ver las diferencias con un componente de servidor.

components/client/PostCard.js

'use client'

import { useState } from 'react'
import Link from 'next/link'

const PostCard = ({ body, id, title }) => {
 const [isOpen, setIsOpen] = useState(false)

 return (
   <article>
     <header>
       <Link href={`/post/${id}`}>
         <h2 style={{ color: 'red', textDecoration: 'underline' }}>{title}</h2>
       </Link>
     </header>
     <button onClick={() => setIsOpen(!isOpen)}>Open</button>
     {isOpen && (
       <div>
         {body}
       </div>
     )}
   </article>
 )
}
export default PostCard

Cambiamos el componente que se referencia desde la lista de posts a este y lo implementamos de la manera más sencilla posible. Sin embargo, esta ya incluye la presencia de hooks de React.

En este momento, bien nuestro editor de código o el terminal en el que ejecutamos la aplicación de Next nos advertirá de que este es un componente de cliente y que lo marquemos como tal con el texto ‘client component’ al principio.


Lejos de ser una mera etiqueta estética, este cambio hará que el compilador de Next lo valore de forma diferente. No tanto en este caso sencillo (de hecho, si vemos el html servido los componentes de cliente se siguen sirviendo desde servidor) pero conforme se complica nuestra aplicación, las ramificaciones pueden ser más profundas (por ejemplo, cuando esta interacción genera conflictos con el marcado de la página servida).

En conclusión

En los últimos dos años, tanto Next como React han sufrido cambios para integrarse mejor entre ellos y para orientar React hacia una tecnología de servidor. En el caso de Next, esto ha resultado en unos componentes más estéticos y con un ciclo de vida más sencillo y mantenible. Nada nuevo en cuanto a funcionalidad, pero sí más coqueto.

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.