JSX for server-side templating

A common setup for Express apps is to configure a template engine to handle rendering views. Alternatively, with some help from Babel you can embed JSX directly into your route definitions, no separate view layer required.

To get started, you'll need to change how you're running your Express server. If you're currently launching the process directly with node server.js (or similar), you'll need to add an extra build step to compile JSX -> JS that your server's Node interpreter can understand.

Add the Babel CLI, "ES2015 modules to CommonJS" Babel transform, and the React JSX Babel transform to your site's dependencies:

Shell

$> npm i -D babel-cli
$> npm i -D babel-plugin-transform-es2015-modules-commonjs
$> npm i -D babel-plugin-transform-react-jsx

Building and running your server should now look like:

Shell

$> npx babel server.js --out-file server-compiled.js --plugins=transform-react-jsx,transform-es2015-modules-commonjs
$> node server-compiled.js

server.js can now use JSX syntax directly in its route definitions. In your existing Express config you might have used app.set('view engine', 'pug'). Now, a request for e.g. the root page of your site might look like:

import { renderToString } from "react-dom/server";
// ... other route handlers ...
app.get("/", (req, res) => {
  res.write("<!DOCTYPE html>");

  const copyrightYear = new Date().getFullYear();

  res.write(
    renderToStaticMarkup(
      <html>
        <head>
          <title>Hello World</title>
        </head>
        <body>
          <p>© {copyrightYear}. All rights reserved.</p>
        </body>
      </html>
    )
  );
  res.end();
});

I'm using React to render the embedded JSX into a string, but React isn't required. You could use Preact, a JSX-only serializer, or roll your own. In fact, this post was inspired by Preact's "render-to-string" helper library.

One thing to consider: using React for serialization makes things easier if you choose to do a client-side rendering pass (in addition to the server pass).

If you like this approach, you can extend it further by moving the inline rendering to a layout function. This is similar to what this site uses behind the scenes:

Javascript

import { renderToStaticMarkup, renderToString } from 'react-dom/server';
function layout({ meta, ContentComponent, componentProps = {}, structuredData = {} }) {
  const { title, description } = meta;

  let structuredDataElem;
  if(Object.keys(structuredData).length > 0) {
    structuredDataElem = (
      <script
        type='application/ld+json'
        dangerouslySetInnerHTML={{__html: JSON.stringify(structuredData)}}
      />
    );
  }

  return renderToStaticMarkup((
    <html>
      <head>
        <title>{ title } | YourSite.com</title>
        <meta charSet='utf-8' />
        <meta name='viewport' content='initial-scale=1.0, width=device-width' />
        <meta name='description' content={description} />
        { structuredDataElems }
        <link rel='stylesheet' href='/css/index.css' />
      </head>
      <body>
        <nav className="navbar"></nav>

        <div id="content"
          dangerouslySetInnerHTML={{__html: renderToString(<ContentComponent {...componentProps} />) }}
        />

        <footer></footer>
      </body>
    </html>
  ));
}

[...]

app.get('/', (req, res) => {
  res.write('<!DOCTYPE html>');

  const meta = { title: 'Home page' };
  res.write(layout({ meta, ContentComponent: HomePage, componentProps: { posts: allPosts } }));
  res.end();
});

© 2021 David Mann. All content provided on this site, code or otherwise, is licensed and/or copyright as specified below: