Renderizando aplicaciones de Redux en el servidor

Renderizar en el servidor una aplicación hecha con React.js nos da una gran mejora de performance, o más bien de percepción de performance, lo cual de cara al usuario se convierte en una mejor UX al parecer que el sitio carga más rápido.

Incluso gracias a renderizar en el servidor es posible hacer aplicaciones que funcionen sin JS (Server First) y que una vez descargado e iniciado JS funcionen como una aplicación de una página (SPA).

Cuando es solo una aplicación en React.js es fácil realizarlo. Pero cuando lo combinamos con Redux necesitamos crear una instancia del Store del mismo en cada petición para que esto funcione.

Instalando dependencias

Primero, como siempre, vamos a instalar nuestras dependencias.

npm i -S react react-dom react-redux redux micro
  • micro: librería para crear micro servicios HTTP.

Preparando el servidor

Una vez instaladas vamos a crear un servidor muy básico. El servidor lo vamos a crear como un micro servicio, de esa forma podemos renderizar nuestra aplicación sin importar si usamos Node.js o no como backend.

// cargamos micro para crear el server
import micro, { json, send, sendError } from 'micro';
async function serverRender(request, response) {
try {
// obtenemos el body del request para saber los datos
const body = await json(request);
    // mandamos la respuesta
return send(response, 200, 'hola mundo');
} catch (error) {
// si hay un error mandamos el error
sendError(request, response, error);
}
}
const server = micro(serverRender);
server.listen(process.env.PORT || 3000);

Ese servidor va a ser nuestra base. Dentro vamos a colocar el código. Pero primero vamos a explicar como va a funcionar esto.

Cuando el usuario entre a nuestra aplicación, digamos que en Django, vamos recibir la petición, Django se tiene que encargar de obtener todos los datos necesarios para nuestra vista y va a mandar una petición HTTP a nuestro micro servicio.

Nuestro micro servicio entonces va a renderizar el HTML que corresponde y va a devolvérselo a Django para que lo inyecte en alguna plantilla HTML y lo mande al usuario, luego ya podríamos renderizar en el navegador y que funcione como una aplicación de una página.

Renderizando React.js

Lo primero es que vamos a definir que datos va a recibir nuestro micro servicio.

type body = {
component: string, // el path al componente
props: Object, // los datos necesarios
};
// ejemplo
const body = {
component: '/build/server/Home.js',
props: {
list: [],
},
};

Ahora que ya sabemos esto vamos a hacer que nuestro micro servicio renderice React.

import micro, { json, send, sendError } from 'micro';
import React from 'react';
import { renderToString } from 'react-dom/server';
async function serverRender(request, response) {
try {
// obtenemos component y props del body
const { component, props } = await json(request);
    // cargamos el componente que recibimos
const Component = require(component);
// lo renderizamos a HTML
const html = renderToString(<Component {...props} />);
    // respondemos con el HTML
return send(response, 200, html);
} catch (error) {
sendError(request, response, error);
}
}
const server = micro(serverRender);
server.listen(process.env.PORT || 3000);

Con esto ya tenemos un pequeño micro servicio que al recibir un request con el componente a renderizar y los datos necesarios devuelve el HTML generado.

Implementando Redux

Si queremos usar Redux en nuestra aplicación, para poder usarlo en el servidor vamos a necesitar instanciar un Store de Redux por cada request y darle ese Store a nuestra aplicación de React.

Para hacer esto podemos hacer que nuestro servidor se encargue de crear el Store y luego de mandárselo a React, pero ya que nuestro micro servicio no sabe que estamos renderizando, solo recibe el componente y los datos y renderiza, no nos sirve hacer esto.

Para nuestro caso lo que vamos a hacer es que el componente que renderizamos reciba los datos genera la instancia del servidor, algo similar a esto:

import React from 'react';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import reducer from 'my-app/reducer.js';
import App from 'my-app/containers/App.jsx';
function ServerProvider(props) {
const store = createStore(reducer, props);
  return (
<Provider store={store}>
<App />
</Provider>
);
}
export default ServerProvider;

De esta forma el entry point de nuestra aplicación para servidor va a ser nuestro componente ServerProvider, el cual va a recibir como props el estado inicial de la aplicación, va a crear el Store y devolver una aplicación de React conectada a Redux.

Con esto hecho de esta forma ya ni siquiera necesitamos modificar nuestro servidor, ya que con solo pasarle el path a nuestro ServerProvider y el estado inicial ya tenemos todo listo para generar el HTML para hacer server-render.

Renderizado con props

Puede pasar, y es muy común, que nuestro componente App reciba sus propios props, datos que no es necesario o no tiene sentido que estén guardados en el estado global (por ejemplo si son inmutables).

En ese caso nuestro micro servicio no nos permite usar esa funcionalidad, así que vamos a modificar tanto el ServerProvider como el micro servicio para poder realizarlo.

function ServerProvider(props) {
const store = createStore(reducer, props.initialState);
  return (
<Provider store={store}>
<App {...props.initialProps} />
</Provider>
);
}

Ahora nuestro ServerProvider va a recibir dos datos vía props, el primero es el initialState el cual es usado para crear el Store de Redux. El segundo es initialProps el cual es usado como los props de nuestro componente App.

async function serverRender(request, response) {
try {
// obtenemos component, initialState e initialProps del body
const {
component,
initialState,
initialProps = {},
} = await json(request);
    // cargamos el componente que recibimos
const Component = require(component);
// lo renderizamos a HTML pasando
// el estado y los props iniciales
const html = renderToString(
<Component
initialState={initialState}
initialProps={initialProps}
/>
);
    // respondemos con el HTML
return send(response, 200, html);
} catch (error) {
sendError(request, response, error);
}
}

Con esta modificación a nuestro micro servicio podemos obtener de los datos que recibimos de la petición el estado y los props iniciales y mandarlos al componente (nuestro ServerProvider) para que renderice la aplicación y gracias al valor por defecto del initialProps en caso de no recibir nada igual va a funcionar.

Conclusión

Como se puede ver renderizar una aplicación con React y Redux no es complicado y solo es necesario realizar un paso más que si usáramos solo React y los beneficios para la UX son muy buenos gracias a que el usuario desde el primer momento puede recibir datos.

Mi recomendación es que siempre rendericen en el servidor sus aplicaciones, incluso que apliquen la metodología Server-First o Progressive Enhancement para que su aplicación no requiere de JS para sus funciones básicas. Con React y Redux renderizar en el servidor es más fácil que nunca.