Table of Contents
Hoy en día tenemos muchas opciones de framework diferentes cuando queremos crear un nuevo proyecto web basado en React. Como desarrollador, puedes encontrarte con la dificultad de saber cuál deberías elegir o cuál se adaptaría mejor a tus necesidades.
Uno de los frameworks más utilizados que quizás conozcas es Next.js, comúnmente utilizado por empresas como Netflix, Twitch o Uber. Está considerado como uno de los frameworks de React con mayor crecimiento.
Ha sido difícil para otros competir con Next.js ya que cubre tres estrategias diferentes de renderizado de páginas, pero desde noviembre de 2021 parece que tenemos un nuevo, fresco y potente framework llamado Remix.
Lo siento Gatsby no me olvidé de ti y me encanta como trabajas al generar sitios estáticos
¿Por qué necesitas Next.js o Remix en lugar de React?
Quizás no, depende. La cuestión es entender cómo funcionan las aplicaciones React, qué problemas podrías encontrar y cómo funcionan frameworks como Next.js o Remix para resolverlos.
El punto sobre React es que puedes hacer Single Page Apps (SPA), donde sólo se utiliza un único archivo HTML para renderizar todas las páginas web y el enrutamiento permanece en el lado del cliente. ¿Pero qué significa esto exactamente?
- Cuando se realiza la solicitud inicial, el navegador recibe una página de caja HTML que contiene todas las solicitudes a la vez.
- No hay contenido pre-renderizado
- Cuando se activa la navegación del usuario, JavaScript reemplaza sólo aquellos componentes y contenidos relacionados con la ruta solicitada, pero el archivo HTML permanece igual.
En resumen, el navegador es el que gestiona qué archivo JavaScript debe cargarse para renderizar la página actual. En otras palabras, Cliente-Side Rendering (CSR).
El CSR puede ser muy elegante y útil para crear aplicaciones que no necesiten preocuparse por el SEO, el almacenamiento en caché o la lentitud del renderizado inicial.
- SSlow Initial Render – Cuando un usuario aterriza por primera vez puede tardar mucho tiempo en cargar la primera página de contenido. Esto significa dejarles una página en blanco hasta que se cargue el JavaScript y se renderice todo.
- SEO – Como sólo tendrá una página HTML el contenido es bastante difícil de conseguir el contenido indexado por los motores de búsqueda y los bots de los medios sociales.
- Problemas de almacenamiento en caché – la estructura HTML no puede ser almacenada en caché, ya que no está disponible en la primera renderización.
Llegados a este punto, puede que pienses que las SPA son el diablo, pero piensa en todas esas aplicaciones internas que utilizan tantas empresas, o en los productos de aplicación que venden.
Next.js o Remix aparecen cuando se quiere aprovechar el funcionamiento de las SPAs sin perder los tres puntos que he mencionado antes. El punto fuerte es que se puede renderizar un sitio web en el lado del servidor antes de enviarlo al cliente.
SSR vs SSG vs ISR
Ya he explicado lo que significa y cómo funciona el CSR, ahora me toca hablar de otras estrategias de renderización de páginas.
Next.js es una opción poderosa, ya que viene con 3 opciones diferentes fuera de la caja, mientras que Remix se basa totalmente en SSR (en este momento). Pero, ¿cómo funcionan exactamente estas estrategias?
Server-Side Rendering (SSR)
Cuando se solicita una página, ésta se renderiza completamente en el servidor antes de ser enviada al cliente. Siendo una gran opción para aquellos sitios web con muchos datos y contenidos dinámicos.
Generación de sitios estáticos (SSG)
Esta estrategia genera todas las páginas por ruta durante el tiempo de construcción. Sólo se envía al cliente la página solicitada cuando se le pide. En otras palabras, esto construye un típico sitio web estático.
Puede ser una opción adecuada para aquellos sitios web estáticos con pocos o ningún dato dinámico y no tantas páginas. Cada cambio en los datos provocará una regeneración de todas las páginas.
Regeneración estática incremental (ISR)
Este concepto ha sido ofrecido por Next.js y es una especie de híbrido de SSR y SSG.
Es como el SSG pero puedes establecer un intervalo de tiempo para saber cuándo deben regenerarse tus páginas. Adecuado para aquellos sitios estáticos con contenido dinámico.
El punto fuerte de Next.js es que te permite cambiar entre esas 3 opciones en cada página, por lo que puedes tener cada una de ellas siguiendo estrategias diferentes.
Así que tal vez te preguntes «por qué debería sustituir Next.js por Remix si sólo funciona con SSR». Y la respuesta es bastante sencilla, hoy en día las aplicaciones y sitios web tienden a tener datos dinámicos y hay pocos ejemplos que se adapten a un escenario SSG o ISR.
Teniendo en cuenta esto, Remix proporciona una API de nivel inferior. Por ejemplo, expone el objeto Request para que puedas modificar fácilmente las cabeceras antes de renderizar la página, mientras que en Next.js necesitarías un middleware para conseguirlo.
Routing
Ambos marcos utilizan un sistema de enrutamiento basado en archivos para renderizar las páginas.
- Cada página permanece en un archivo diferente.
- Cada página está asociada a una ruta en función de su nombre de archivo.
- Cada página, al final, renderiza un componente React.
Routing en Remix
Añadirás cada ruta en /app/routes
y cada archivo o carpeta creada dentro de ella será tratada como una ruta.
Además, encontrarás un archivo en/app/root.jsx
que es la «ruta raíz» y es el diseño de toda la aplicación. Contiene las etiquetas,y, así como otras cosas relacionadas con Remix.
Routing en Next.js
Añadirás cada ruta en /pages
y funciona exactamente igual que Remix.
En este caso, puede, o no, tener un pages/_app.js
y un pages/_document.js
. Donde App se utiliza para inicializar las páginas y Document contiene las, y etiquetas.
Next.js los utiliza por defecto sin necesidad de configuración extra. Pero, en caso de que necesites personalizarlos puedes crear esos dos archivos, anulando así los que vienen por defecto.
Rutas de índice
Ambas utilizan el mismo sistema para representar una ruta de contenedores.
Ejemplo con Next.js:
pages/index.js
=>/
(root page)pages/posts/index.js
ORpages/posts.js
=>/posts
Ejemplo con Remix:
routes/index.js
=>/
(root page)routes/posts/index.js
ORroutes/posts.js
=>/posts
Rutas anidadas
Remix está construido sobre React Router y por el mismo equipo, así que puedes imaginar quién es el ganador cuando se trata de anidar rutas. La idea detrás es hacer que una ruta activa monte un diseño que contenga múltiples rutas anidadas, de modo que cada ruta gestione su propio contenido.
Si tuviera que explicarlo con palabras de React te diría que imaginaras que cada ruta anidada son componentes y que dependiendo de la ruta de la URL se pueden montar y desmontar.
Para lograr esto, Remix usa el <Outlet />
componente React-router. Se utiliza en las rutas padre para indicarles que deben renderizar los elementos de las rutas hijo siempre que se activen.
Recuerda que todas estas rutas anidadas están precargadas en el servidor por lo que casi todos los estados de carga pueden ser eliminados. No suena como una mezcla entre SSR y SPA?
Por otro lado, Next.js sí admite rutas anidadas, pero funcionan como una página independiente. Si quieres conseguir algo similar a lo que hace Remix probablemente debas personalizar tu _app.js
.
Ejemplo con ambos:
routes/posts/news/index.js
=>/posts/news
Rutas dinámicas
Funcionan renderizando diferentes contenidos basados en un parámetro de URL que es dinámico, pero utiliza un único archivo de ruta. En este caso, ambos frameworks soportan esta funcionalidad pero utilizan diferentes convenciones de nomenclatura.
Ejemplo con Next.js
pages/posts/[postId].js
=>/posts/some-post-slug
pages/products/[category]/[productId].js
=>/products/keyboards/some-keyboard-slug
O/products/headphones/some-headphone-slug
Tiene su propio gancho, useRouter
que le proporcionará el valor actual de esos parámetros dinámicos (postId, category, productId).
Ejemplo con Remix
/app/routes/posts/$postId.js
=>/posts/some-post-slug
/app/routes/products/$category/$productId.js
=>/products/keyboards/some-keyboard-slug
OR/products/headphones/some-headphone-slug
Podrías usar useParam
el React Router hook para acceder a esos parámetros dinámicos.
¿Qué pasa con las rutas que necesitan cargar archivos?
Imagina que necesitas que tu sitio web contenga los típicos archivos robots.txt
and sitemap.xml
Tanto en Next.js como en Remix podrías añadirlos al directorio /public. Pero Remix te permite lograrlo a través del sistema de enrutamiento, simplemente creando un/app/routes/[sitemap.xml].js
o /app/routes/[robots.txt].js
Carga de datos
Ambos marcos son del lado del servidor, por lo que cada uno tiene diferentes sistemas para obtener datos y realizar llamadas a la API.
En el caso de Next.js puedes elegir entre dos opciones, dependiendo del tipo de página que quieras construir.
- getStaticProps (SSG, ISR si se establece el intervalo de revalidación) – obtiene los datos en el momento de la construcción y proporciona sus datos como los props de los componentes de la página.
export async function getStaticProps( {params} ) {
const productList = await getProductList()
return {
props: {
productList,
slug: params.slug
}
}
}
- getServerSideProps (SSR) – obtiene datos del lado del servidor en tiempo de ejecución y proporciona los datos devueltos como props del componente de la página.
export async function getStaticProps( {params} ) {
const productList = await getProductList()
return {
props: {
productList,
slug: params.slug
}
}
}
/**
* Here you would have your own getStaticProps/getServerSideProps functions
* depending on what system you choose.
**/
const PageName = (props)=> {
const { productList } = props
// Here you could render the requested data
return (<div>
<pre> {JSON.stringify(productList, null, 4)} </pre>
</div>)
}
Remix utiliza una forma diferente de cargar los datos
- Te proporciona una función
loader
para usar en cada archivo de ruta que funcionará en el lado del servidor en tiempo de ejecución. - En el propio componente de la página, podrás utilizar el hook
useLoaderData
para recoger estos datos.
/**
* Here you would have your own getStaticProps/getServerSideProps functions
* depending on what system you choose.
**/
export const async loader = ( {params} ) => {
const productList = await getProductList()
return {
productList,
slug: params.slug
}
};
export default function PageName() {
const { productList } = useLoaderData();
// Here you could render the requested data
return (<div>
<pre> {JSON.stringify(productList, null, 4)} </pre>
</div>)
}
Mutaciones de datos
Next.js no tiene una forma incorporada para realizar mutaciones y tienes que hacerlo manualmente. Remix, por otro lado, ha creado un componente de formulario y lo utiliza como lo haría con el elemento de formulario HTML nativo del navegador. Si no introduces ninguna URL de acción utilizará la misma como ruta para el formulario.
Si el método es un GET, se activará el loader
mientras que si el método es un POST, se activará la función action
exportada definida en el componente.
Además, puedes utilizar el hook useTransition proporcionado para gestionar el comportamiento de la aplicación en función del estado de la solicitud sin tener que gestionarlo manualmente como es habitual.
export const loader = async ({ params }) => {
// Each time this page receive a GET Request, this loader will be executed
const userData = await getSomeUserData(params.slug)
return {
userData // it contains the User Name
}
}
export const action = async ({ request }) => {
// Each time this page receive a POST Request, this loader will be executed
const form = await request.formData()
const content = form.get('content')
return redirect('/')
}
export default function App() {
const { name } = useLoaderData();
return (
<div>
<div>{`Hello there ${name}`}</div>
<Form method="POST">
<label htmlFor="content">
Content: <textarea name="content" />
</label>
<input type="submit" value="Add New" />
</Form>
</div>
)
}
Estilismo
Next.js viene con soporte de CSS incorporado fuera de la caja para que puedas importar directamente en tu /pages/_app.js cualquier archivo style.css que necesites.
También puedes añadir otros frameworks CSS, con alguna configuración o plugins es bastante fácil de conseguir.
Remix prefiere centrarse en una solución más respetuosa con los estándares web, proporcionando un componente y una función de exportación de links.
import styles from "./styles/app.css";
export function links() {
return [
{ rel: "stylesheet", href: styles },
{ rel: "stylesheet", href: 'https://.....' },
];
}
export default function App() {
return (
<html lang="en">
<head>
<Links />
</head>
<body>
<Outlet />
</body>
</html>
);
}
Podrías pensar que esto es demasiado para simplemente cargar una hoja de estilos pero, ¿qué tal si te digo que puedes usarlo en cada página, por separado, permitiéndote cargar estilos/fondos específicos sólo cuando sea necesario?
También aquí puedes utilizar otros frameworks CSS con poca configuración, como Tailwindcss.
Optimización de imágenes
Aquí Next.js gana ya que tiene su propio componente next/image
que permite la optimización de imágenes, lazy loading, control de tamaño e integra cargadores. Desafortunadamente, en este momento Remix no tiene ningún soporte de optimización de imágenes.
SEO
Al principio de este artículo, mencioné que uno de los puntos detrás de estos frameworks es resolver los problemas de SEO de los SPAs.
Ambos tienen sus propios mecanismos incorporados para gestionar dinámicamente qué metadatos deben utilizarse en cada página, como palabras clave, título, descripción, etc.
En Next.js puedes utilizar su componente incorporado next/head
importándolo en tus páginas/rutas.
import Head from 'next/head'
export default function PostPage() {
return (
<div>
<Head>
<title>Post Page Title</title>
<meta name="description" content="Post Page Description" />
</Head>
<p>
All the metadata inside Head component will be injected into the
documents head
</p>
</div>
)
}
Por otro lado, Remix nos proporciona una funciónmeta
exportada que puede recibir los datos solicitados de la página así como los parámetros de la URL.
export const meta = ({ data, params }) => {
charset: "utf-8",
viewport: "width=device-width,initial-scale=1",
title: `Post | ${data.title}`,
description: data.description,
};
export default function PostPage() {
return (
<div>
<p>
All the metadata returned on meta function will be injected into the
documents head
</p>
</div>
)
}
Gestión de errores
Next.js te da la posibilidad de definir páginas personalizadas cuando se detectan ciertos errores, como los errores 400 o 500. Pero cualquier otro error más allá de los errores de routing o de estado haría que la página se rompiera.
Remix lo deja a tu imaginación para que puedas cubrir y personalizar todos los errores que quieras. Han añadido Error Boundary, un concepto único para el manejo de errores – introducido en React v16.
Puede cubrir esos errores fácilmente utilizando las funciones exportadas CatchBoundary
y ErrorBoundary
en su root.tsx
o incluso en cada página. Vamos a añadir algunos ejemplos visuales de esto.
// Here you will catch any controlled error and will tell the framework what information should be shown
const CatchBoundary = () => {
let caught = useCatch();
switch (caught.status) {
case 401:
// Document is a custom React Component which contains <html>, <head>,<body>, etc
return (
<Document title={`${caught.status} ${caught.statusText}`}>
<h1>
{caught.status} {caught.statusText}
</h1>
</Document>
);
case 404:
return <PageNotFoundComponent />; // Yes, you can also use a React Component
default:
throw new Error(
`Unexpected caught response with status: ${caught.status}`
);
}
};
// Here you will have a more generic advise for when an uncontrolled error has been triggered
const ErrorBoundary = ( { error } ) => {
return (
<Document title="Uh-oh!">
<h1>App Error</h1>
<pre>{error.message}</pre>
<p>
Replace this UI with what you want users to see when your app throws
uncaught errors.
</p>
</Document>
);
};
export const loader: LoaderFunction = async ({ params }) => {
const post = await getPost(params.slug);
if (!post) {
throw new Response("Not Found", {
status: 404,
});
}
return json({ post });
};
export default function PostPage() {
const { post } = useLoaderData();
return (
<div>
<pre>
{JSON.stringify(post, null, 4)}
</pre>
</div>
)
}
Despliegue
Next.js es fácil de desplegar en cualquier servidor que soporte Node.js. También tiene una integración para desplegar como serverless a Vercel, y Netlify tiene su propio adaptador para que puedas desplegar fácilmente tu aplicación.
Remix está construido sobre la API Web Fetch en lugar de Node.js por lo que puede funcionar en Vercel, Netlify, Architect (servidores Node.js) así como en entornos no Node.js como Cloudflare.
También le proporciona adaptadores listos para usar cuando inicia su proyecto, de modo que puede ir directamente al grano si sabe dónde va a desplegar su proyecto.
Y, una de las cosas más importantes de Remix es que ya no utiliza Webpack. En su lugar, utiliza Esbuild que es extremadamente rápido en la agrupación y el despliegue.
Otras menciones
Hay otros puntos a tener en cuenta a la hora de elegir Next.js o Remix pero he querido recoger los que, en mi opinión, tienen más impacto a la hora de desarrollar un proyecto. Pero si quieres saber un poco más, aquí tienes una pequeña recapitulación de cosas de las que no he hablado en este artículo.
- Live Reload. Next.js utiliza Hot Module Reloading (HMR) y está activado por defecto, mientras que en Remix tienes que añadir un componente en tu root.tsx para forzar que tu aplicación se recargue cada vez que se cambie el código.
- Remix soporta Cookies, Sesiones y Autenticación mientras que Next.js no lo hace – debes usar librerías externas.
- Next.js tiene soporte incorporado para la optimización de fuentes y scripts, mientras que Remix no lo tiene.
- Ambos le permiten utilizar Typescript desde el principio.
- Remix trabaja la mayoría de sus funcionalidades sin ejecutar JavaScript mientras que Next.js no lo hace.
- Remix puede contener rutas independientes dentro de otras anidadas
- Ambos pueden trabajar rápidamente con Tailwindcss y tienen su propia guía para lograrlo.
- Next.js tiene soporte incorporado para el routing internacionalizado.
- Next.js es compatible con AMP mientras que Remix no lo es.