Desmitificando la representación del lado del servidor en React

Echemos un vistazo más de cerca a la función que le permite crear aplicaciones universales con React .

La representación del lado del servidor (SSR a partir de ahora) es la capacidad de un marco de front-end para representar el marcado mientras se ejecuta en un sistema de back-end .

Las aplicaciones que tienen la capacidad de renderizarse tanto en el servidor como en el cliente se denominan aplicaciones universales .

¿Por qué molestarse?

Para comprender por qué se necesita SSR, debemos comprender la evolución de las aplicaciones web en los últimos 10 años.

Esto está estrechamente relacionado con el auge de la aplicación de página única - SPA a partir de ahora . Los SPA ofrecen grandes ventajas en velocidad y UX sobre las aplicaciones tradicionales renderizadas por servidor.

Pero hay una trampa. La solicitud inicial del servidor generalmente devuelve un archivo HTML vacío con un montón de enlaces CSS y JavaScript (JS). Luego, los archivos externos deben buscarse para generar el marcado relevante.

Esto significa que el usuario tendrá que esperar más para el renderizado inicial . Esto también significa que los rastreadores pueden interpretar su página como vacía.

Entonces, la idea es renderizar su aplicación en el servidor inicialmente, luego aprovechar las capacidades de los SPA en el cliente.

SSR + SPA = Aplicación universal *

* Encontrará el término aplicación isomórfica en algunos artículos; es lo mismo.

Ahora, el usuario no tiene que esperar a que se cargue su JS y obtiene un HTML completamente renderizado tan pronto como la solicitud inicial devuelve una respuesta.

Imagine la gran mejora para los usuarios que navegan en redes 3G lentas. En lugar de esperar más de 20 segundos para que se cargue el sitio web, obtiene contenido en su pantalla casi al instante.

Y ahora, todas las solicitudes que se realizan a su servidor devuelven HTML completamente renderizado. ¡Buenas noticias para tu departamento de SEO!

Los rastreadores ahora verán su sitio web como cualquier otro sitio estático en la web e indexarán todo el contenido que representa en el servidor.

Entonces, para recapitular, los dos principales beneficios que obtenemos de SSR son:

  • Tiempos más rápidos para el procesamiento de la página inicial
  • Páginas HTML totalmente indexables

Comprensión de SSR: un paso a la vez

Tomemos un enfoque iterativo para construir nuestro ejemplo completo de SSR. Comenzamos con la API de React para la representación del servidor y agregaremos algo a la mezcla en cada paso.

Puede seguir este repositorio y las etiquetas definidas allí para cada paso.

Configuración básica

Lo primero es lo primero. ¡Para usar SSR, necesitamos un servidor! Usaremos una aplicación Express simple que renderizará nuestra aplicación React.

import express from "express"; import path from "path"; import React from "react"; import { renderToString } from "react-dom/server"; import Layout from "./components/Layout"; const app = express(); app.use( express.static( path.resolve( __dirname, "../dist" ) ) ); app.get( "/*", ( req, res ) => { const jsx = (  ); const reactDom = renderToString( jsx ); res.writeHead( 200, { "Content-Type": "text/html" } ); res.end( htmlTemplate( reactDom ) ); } ); app.listen( 2048 ); function htmlTemplate( reactDom ) { return `     React SSR ${ reactDom } `; }

Necesitamos decirle a Express que sirva nuestros archivos estáticos desde nuestra carpeta de salida - línea 10.

Creamos una ruta que maneja todas las solicitudes entrantes no estáticas. Esta ruta responderá con el HTML renderizado.

Usamos renderToString- líneas 13-14 - para convertir nuestro JSX inicial en un stringque insertamos en la plantilla HTML.

Como nota, estamos usando los mismos complementos de Babel para el código del cliente y para el código del servidor. Entonces los módulos JSX y ES funcionan en el interior server.js.

El método correspondiente en el cliente es ahora ReactDOM.hydrate. Esta función utilizará la aplicación React renderizada por el servidor y adjuntará controladores de eventos.

import ReactDOM from "react-dom"; import Layout from "./components/Layout"; const app = document.getElementById( "app" ); ReactDOM.hydrate( , app );

Para ver el ejemplo completo, consulte la basicetiqueta en el repositorio.

¡Eso es! ¡Acabas de crear tu primera aplicación React renderizada por servidor !

Reaccionar enrutador

Tenemos que ser honestos aquí, la aplicación no hace mucho. Así que agreguemos algunas rutas y veamos cómo manejamos la parte del servidor.

import { Link, Switch, Route } from "react-router-dom"; import Home from "./Home"; import About from "./About"; import Contact from "./Contact"; export default class Layout extends React.Component { /* ... */ render() { return ( 

{ this.state.title }

Home About Contact ); } }

El Layoutcomponente ahora representa múltiples rutas en el cliente.

Necesitamos imitar la configuración del enrutador en el servidor. A continuación puede ver los principales cambios que se deben realizar.

/* ... */ import { StaticRouter } from "react-router-dom"; /* ... */ app.get( "/*", ( req, res ) => { const context = { }; const jsx = (    ); const reactDom = renderToString( jsx ); res.writeHead( 200, { "Content-Type": "text/html" } ); res.end( htmlTemplate( reactDom ) ); } ); /* ... */

En el servidor, debemos envolver nuestra aplicación React en el StaticRoutercomponente y proporcionar el location.

Como nota al margen, contextse usa para rastrear posibles redireccionamientos mientras se procesa React DOM. Esto debe manejarse con una respuesta 3XX del servidor.

El ejemplo completo se puede ver en la routeretiqueta del mismo repositorio.

Redux

Ahora que tenemos capacidades de enrutamiento, integremos Redux.

En el escenario simple, necesitamos que Redux maneje la administración del estado en el cliente. Pero, ¿qué pasa si necesitamos representar partes del DOM en función de ese estado? Tiene sentido inicializar Redux en el servidor.

If your app is dispatchingactions on the server, it needs to capture the state and send it over the wire together with the HTML. On the client, we feed that initial state into Redux.

Let’s have a look at the server first:

/* ... */ import { Provider as ReduxProvider } from "react-redux"; /* ... */ app.get( "/*", ( req, res ) => { const context = { }; const store = createStore( ); store.dispatch( initializeSession( ) ); const jsx = (      ); const reactDom = renderToString( jsx ); const reduxState = store.getState( ); res.writeHead( 200, { "Content-Type": "text/html" } ); res.end( htmlTemplate( reactDom, reduxState ) ); } ); app.listen( 2048 ); function htmlTemplate( reactDom, reduxState ) { return ` /* ... */ ${ reactDom } window.REDUX_DATA = ${ JSON.stringify( reduxState ) }   /* ... */ `; }

It looks ugly, but we need to send the full JSON state together with our HTML.

Then we look at the client:

import React from "react"; import ReactDOM from "react-dom"; import { BrowserRouter as Router } from "react-router-dom"; import { Provider as ReduxProvider } from "react-redux"; import Layout from "./components/Layout"; import createStore from "./store"; const store = createStore( window.REDUX_DATA ); const jsx = (      ); const app = document.getElementById( "app" ); ReactDOM.hydrate( jsx, app );

Notice that we call createStore twice, first on the server, then on the client. However, on the client, we initialize the state with whatever state was saved on the server. This process is similar to the DOM hydration.

The full example can be seen on the redux tag in the same repository.

Fetch Data

The final piece of the puzzle is loading data. This is where it gets a bit trickier. Let’s say we have an API serving JSON data.

In our codebase, I fetch all the events from the 2018 Formula 1 season from a public API. Let’s say we want to display all the events on the Home page.

We can call our API only from the client after the React app is mounted and everything is rendered. But this will have a bad impact on UX, potentially showing a spinner or a loader before the user sees relevant content.

We already have Redux, as a way of storing data on the server and sending it over to the client.

What if we make our API calls on the server, store the results in Redux, and then render the full HTML with the relevant data for the client?

But how can we know which calls need to be made?

First, we need a different way of declaring routes. So we switch to the so-called routes config file.

export default [ { path: "/", component: Home, exact: true, }, { path: "/about", component: About, exact: true, }, { path: "/contact", component: Contact, exact: true, }, { path: "/secret", component: Secret, exact: true, }, ];

And we statically declare the data requirements on each component.

/* ... */ import { fetchData } from "../store"; class Home extends React.Component { /* ... */ render( ) { const { circuits } = this.props; return ( /* ... */ ); } } Home.serverFetch = fetchData; // static declaration of data requirements /* ... */

Keep in mind that serverFetch is made up, you can use whatever sounds better for you.

As a note here, fetchData is a Redux thunk action, returning a Promise when dispatched.

On the server, we can use a special function from react-router, called matchRoute.

/* ... */ import { StaticRouter, matchPath } from "react-router-dom"; import routes from "./routes"; /* ... */ app.get( "/*", ( req, res ) => { /* ... */ const dataRequirements = routes .filter( route => matchPath( req.url, route ) ) // filter matching paths .map( route => route.component ) // map to components .filter( comp => comp.serverFetch ) // check if components have data requirement .map( comp => store.dispatch( comp.serverFetch( ) ) ); // dispatch data requirement Promise.all( dataRequirements ).then( ( ) => { const jsx = (      ); const reactDom = renderToString( jsx ); const reduxState = store.getState( ); res.writeHead( 200, { "Content-Type": "text/html" } ); res.end( htmlTemplate( reactDom, reduxState ) ); } ); } ); /* ... */

With this, we get a list of components that will be mounted when React is rendered to string on the current URL.

We gather the data requirements and we wait for all the API calls to return. Finally, we resume the server render, but with data already available in Redux.

The full example can be seen on the fetch-data tag in the same repository.

You probably notice that this comes with a performance penalty, because we’re delaying the render until the data is fetched.

This is where you start comparing metrics and do your best to understand which calls are essential and which aren’t. For example, fetching products for an e-commerce app might be crucial, but prices and sidebar filters can be lazy loaded.

Helmet

As a bonus, let’s look at SEO. While working with React, you may want to set different values in your ad> tag. For example, you may want to se t the t itle, meta tags, keywords, and so on.

Keep in mind that the ad> tag is normally not part of your React app!

react-helmet has you covered in this scenario. And it has great support for SSR.

import React from "react"; import Helmet from "react-helmet"; const Contact = () => ( 

This is the contact page

Contact Page ); export default Contact;

You just add your head data anywhere in your component tree. This gives you support for changing values outside the mounted React app on the client.

And now we add the support for SSR:

/* ... */ import Helmet from "react-helmet"; /* ... */ app.get( "/*", ( req, res ) => { /* ... */ const jsx = ( ); const reactDom = renderToString( jsx ); const reduxState = store.getState( ); const helmetData = Helmet.renderStatic( ); res.writeHead( 200, { "Content-Type": "text/html" } ); res.end( htmlTemplate( reactDom, reduxState, helmetData ) ); } ); } ); app.listen( 2048 ); function htmlTemplate( reactDom, reduxState, helmetData ) { return ` ${ helmetData.title.toString( ) } ${ helmetData.meta.toString( ) } React SSR /* ... */ `; }

And now we have a fully functional React SSR example!

We started from a simple render of HTML in the context of an Express app. We gradually added routing, state management, and data fetching. Finally, we handled changes outside the scope of the React application.

The final codebase is on master on the same repository that was mentioned before.

Conclusion

As you’ve seen, SSR is not a big deal, but it can get complex. And it’s much easier to grasp if you build your needs step by step.

Is it worth adding SSR to your application? As always, it depends. It’s a must if your website is public and accessible to hundreds of thousands of users. But if you’re building a tool/dashboard-like application it might not be worth the effort.

However, leveraging the power of universal apps is a step forward for the front-end community.

Do you use a similar approach for SSR? Or you think I missed something? Drop me a message below or on Twitter.

If you found this article useful, help me share it with the community!