A continuación, te presento una versión mejorada y más estructurada del blog, con explicaciones adicionales, ejemplos claros y un formato más amigable:


Cómo Implementar Renderizado del Lado del Servidor (SSR) en React con Vite y Express: Una Guía Paso a Paso

El renderizado del lado del servidor (SSR) es una técnica que permite generar HTML en el servidor y enviarlo al cliente. Esto mejora el SEO, acelera la carga inicial y optimiza la experiencia del usuario, especialmente en dispositivos con conexiones lentas. En este tutorial aprenderás a construir una aplicación React con SSR usando Vite y Express, sin depender de frameworks pesados como Next.js.


Introducción al SSR en React

SSR permite que el contenido de la aplicación se renderice en el servidor, lo que significa que los motores de búsqueda pueden indexar el HTML generado y los usuarios pueden ver contenido útil incluso antes de que se ejecute JavaScript. Esto se traduce en:

  • Mejor SEO: Los crawlers reciben contenido completo.
  • Carga más rápida: El usuario ve el contenido renderizado inmediatamente.
  • Seguridad de datos: Las llamadas a APIs y el manejo de información sensible se realizan en el servidor.

Paso 1: Configuración Inicial del Proyecto

1.1. Inicializar el Proyecto e Instalar Dependencias

Crea un nuevo proyecto e instala las dependencias esenciales:

npm init -y
npm install react react-dom express
npm install vite @vitejs/plugin-react -D

1.2. Configurar Scripts en package.json

Agrega los siguientes scripts para facilitar el desarrollo y la producción:

"scripts": {
  "dev": "node server-dev.js",
  "build:client": "vite build --outDir dist/client",
  "build:server": "vite build --ssr src/entry-server.jsx --outDir dist/server",
  "build": "npm run build:client && npm run build:server",
  "serve": "node server-prod.js"
}

1.3. Habilitar Módulos ES

Para utilizar módulos ES6, añade "type": "module" en tu package.json:

{
  "name": "tu-proyecto",
  "type": "module",
  // ...
}

Paso 2: Estructura de Archivos y Componentes

2.1. Plantilla HTML (index.html)

Crea un archivo index.html que servirá como base de la aplicación. Observa el marcador <!--outlet-->, el cual se reemplazará con el HTML renderizado en el servidor:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>React SSR con Vite y Express</title>
  </head>
  <body>
    <div id="app"><!--outlet--></div>
    <script type="module" src="/src/entry-client.jsx"></script>
  </body>
</html>

2.2. Componente Principal (App.jsx)

Define el componente principal de React que se renderizará tanto en el cliente como en el servidor:

import { useState } from 'react';

const App = ({ data }) => {
  const [count, setCount] = useState(0);

  return (
    <main>
      <h1>Aplicación con SSR</h1>
      <p>Contador: {count}</p>
      <button onClick={() => setCount(count + 1)}>Incrementar</button>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </main>
  );
};

export default App;

Paso 3: Configuración de Vite

Crea el archivo vite.config.js para integrar React con Vite:

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
});

Paso 4: Puntos de Entrada para SSR

4.1. Punto de Entrada del Cliente (entry-client.jsx)

Este archivo se encarga de hidratar el HTML generado en el servidor, conectándolo con React en el navegador:

import { hydrateRoot } from 'react-dom/client';
import App from './App';

const data = window.__data__; // Datos inyectados desde el servidor

hydrateRoot(document.getElementById('app'), <App data={data} />);

4.2. Punto de Entrada del Servidor (entry-server.jsx)

Aquí se convierte el componente React a una cadena de HTML que se enviará al cliente:

import { renderToString } from 'react-dom/server';
import App from './App';

export const render = (data) => {
  return renderToString(<App data={data} />);
};

Paso 5: Configuración de los Servidores

5.1. Servidor de Desarrollo (server-dev.js)

Utiliza el middleware de Vite para un desarrollo con recarga en caliente (hot-reload):

import express from 'express';
import fs from 'fs';
import { createServer } from 'vite';

const app = express();
const vite = await createServer({ server: { middlewareMode: true }, appType: 'custom' });

app.use(vite.middlewares);

app.use('*', async (req, res) => {
  try {
    // Lee y transforma la plantilla HTML
    const template = await vite.transformIndexHtml(req.url, fs.readFileSync('index.html', 'utf-8'));
    // Carga el módulo SSR de forma dinámica
    const { render } = await vite.ssrLoadModule('/src/entry-server.jsx');
    
    // Opcional: cargar datos del servidor (ver Paso 6)
    const html = template.replace('<!--outlet-->', render());
    res.status(200).send(html);
  } catch (error) {
    res.status(500).send(error.message);
  }
});

app.listen(4173, () => console.log('Servidor de desarrollo: http://localhost:4173'));

5.2. Servidor de Producción (server-prod.js)

Sirve los archivos estáticos compilados y utiliza el módulo SSR generado durante el build:

import express from 'express';
import fs from 'fs';
import path from 'path';

const app = express();

// Servir archivos estáticos del cliente compilado
app.use(express.static('dist/client'));

app.use('*', async (_, res) => {
  // Lee la plantilla HTML ya procesada
  const template = fs.readFileSync('./dist/client/index.html', 'utf-8');
  const { render } = await import('./dist/server/entry-server.js');
  
  // Opcional: cargar datos del servidor (ver Paso 6)
  res.send(template.replace('<!--outlet-->', render()));
});

app.listen(5173, () => console.log('Servidor de producción: http://localhost:5173'));

Paso 6: Incorporando Datos en el Servidor

Para hacer la aplicación más dinámica, es común realizar llamadas a APIs en el servidor. Esto permite inyectar datos en el HTML renderizado.

6.1. Función para Obtener Datos (function.js)

Crea una función para realizar peticiones a una API externa:

export const getServerData = async () => {
  const response = await fetch('https://dummyjson.com/products/1');
  return response.json();
};

6.2. Integración de la Función en los Servidores

Modifica tanto el servidor de desarrollo como el de producción para inyectar los datos obtenidos:

En el servidor de desarrollo (server-dev.js):

// Cargar la función de obtención de datos
const { getServerData } = await vite.ssrLoadModule('/src/function.js');

const data = await getServerData();
const script = `<script>window.__data__ = ${JSON.stringify(data)}</script>`;
const html = template.replace('<!--outlet-->', `${render(data)} ${script}`);

En el servidor de producción (server-prod.js):

// Cargar la función desde la versión compilada
const { getServerData } = await import('./dist/function/function.js');

const data = await getServerData();
const script = `<script>window.__data__ = ${JSON.stringify(data)}</script>`;
const html = template.replace('<!--outlet-->', `${render(data)} ${script}`);

Conclusión y Beneficios del SSR

Mediante esta implementación, hemos conseguido:

  • Mejorar el SEO: El HTML pre-renderizado permite que los motores de búsqueda indexen tu contenido con facilidad.
  • Optimizar la carga inicial: Los usuarios ven contenido renderizado al instante, sin esperar la hidratación de React.
  • Aumentar la seguridad: Las llamadas a APIs se realizan en el servidor, protegiendo claves y lógica sensible.

¿Qué puedes probar?

  • Desactiva JavaScript en tu navegador: Verás que el contenido permanece visible, gracias al SSR.
  • Experimenta integrando otras fuentes de datos: Amplía la funcionalidad de tu aplicación y refina la experiencia del usuario.

Esta guía paso a paso demuestra cómo, con herramientas modernas y una configuración minimalista, puedes implementar SSR en una aplicación React sin recurrir a frameworks pesados. ¡Atrévete a experimentar y lleva tus aplicaciones al siguiente nivel!