Generate HTML Emails with React on the Server

Subscribe to my newsletter and never miss my upcoming articles

This might seem strange and some questions probably popped up in your head. Why would you want to use JSX for emails? Can you actually render JSX on the server?

Why JSX?

So... JSX is really good for templating. You can simply do something like this:

function Email({ heading }){
  return(
    <h1>{heading}</h1>
  )
}

This makes populating email templates with data really easy, you just pass in props like you would with any React component.

It also gives you the benefit of having the JSX code highlighted, something which you don't get with HTML strings.

Can you render JSX on the server?

Yes. It is actually quite common. This is what Gatsby and NextJS do to provide statically generated content and server-side rendering.

Now, we don't need a solution as complicated as Gatsby or NextJS to render JSX on the server. We just need React.

How to do it?

To be able to render JSX on the server we need two packages, React and ReactDOM. Along with this, we also need a bundler that supports JSX syntax.

Choosing bundler

So, the most obvious choice would be Webpack, but I'm not a fan of how complicated it is to get JSX support.

I would normally use Typescript, which has support for JSX out of the box, but I'd like to keep this article as simple as possible. I want to provide a working example for people that don't use Typescript as well.

The option I'll go for is esbuild. It is really performant and easy to use with great documentation.

Installing dependencies

Create a new project directory and run npm init -y to get a boilerplate package.json.

Now, let's install React and ReactDOM.

yarn add react react-dom

We'll also install esbuild as a development dependency.

yarn add -D esbuild

The code

If you are familiar with React, which I assume you are if you are reading this article, the code is going to be rather straight forward.

Let's start by creating a directory called src.

mkdir src

Inside that directory, create a file called email.jsx. That file should contain the following code:

import React from "react"
import ReactDOMServer from "react-dom/server"

export default function getEmail(props){
  const email = <Email {...props} />
  return ReactDOMServer.renderToStaticMarkup(email)
}

function Email({ heading, content }){
  return<>
    <h1>{heading}</h1>
    <p>{content}</p>
  </>
}

The thing to note here is the ReactDOMServer.renderToStaticMarkup(). This method will create an HTML string from our JSX. This can then be used as the email body.

Also, you should know that there is another method called ReactDOMServer.renderToString(). The difference between these two methods is that the latter allows you to hydrate the page. That basically means that you can render the initial page as static HTML and then add React to render further components.

The ReactDOMServer.renderToStaticMarkup() method doesn't allow this, and that's fine. We don't need to be able to hydrate the DOM since our emails shouldn't include any JS.

Now, we'll create a file called index.js inside the src/ directory. It should look like this:

import getEmail from "./email";

async function sendEmail(){
    const body = getEmail({ 
        heading: "Newsletter",
        content: "Welcome to my newsletter. Read on to find interesting articles and cool development tips." 
    });

    console.log(body);
}

sendEmail();

For now, we'll simply log the result of the getEmail function. The important thing to note is that we can simply pass in an object that contains our email data to getEmail.

This is actually everything we need to get a working templating engine. You could create more jsx files with different templates or create a main template file which is then used as a wrapper around other email templates.

esbuild

You can't actually run the above code yet, partly because it uses import syntax, and partly because it contains JSX. So, to run it we need to build our code using esbuild.

Open your package.json and add a build script, like this:

"scripts": {
    "build": "esbuild src/index.js --bundle --platform=node --outdir=dist"
  },
  • src/index.js tells esbuild where to find the source files. In our case, that's our index.js file.

  • --bundle tells esbuild to put all of the dependencies into a single output file.

  • --platform=node specifies that we want our code to run in a node.js environment and not in the browser.

  • --outdir=dist tells esbuild where the output should be placed.

And then run:

yarn build

or, if you're not using yarn:

npm run build

Take a look at your project files, you should now see a dist folder. If you open dist/index.js, you should see that react and react-dom has been bundled together with our code.

You can also search for email.jsx to see the output of our JSX file. You'll notice that it has been transformed into react.default.createElement() calls. That default is a result of the bundling, it would normally look like React.createElement().

This can now be executed using NodeJS.

Run the code

To actually run the code, we can do this from your project directory:

node dist/index

This should output something like this:

<h1>Newsletter</h1>
<p>Welcome to my newsletter. Read on to find interesting articles and cool development tips.</p>

Conclusion

Now, this output would, of course, be used in an email body and not just be logged. Though I'll show you how to actually send one of these emails with AWS Simple Email Service in a future article.

So be sure to subscribe to my newsletter here on Hashnode and/or follow me on Twitter to get notified when that article is published.

Thanks for reading!

Comments (2)

shadowtime2000's photo

Cool, but really this pattern and stuff can be used for anything, not just emails and it may be better to just use template engines instead so you don't have to use a bundler.

Carl Hallén Jansson's photo

Yeah, I don't disagree. I just wanted to show how you can use familiar frontend tools to build HTML emails.

And normally, I wouldn't even have to use a bundler for this, since I'm usually using Typescript.

Using template engines is totally valid and arguably easier to use, but I've found that I prefer to use React as a templating engine, and I thought others might be interested doing so too.