14/12/2022 4 Minutes read Tech 

Writing good looking emails is usually not the first task of your product backlog, but eventually, every product ends up needing it.

If you ever looked up at what an email’s source code looks like, you wouldn’t even consider writing it in plain HTML (really, you can’t be that crazy)…

<!-- [...] -->

<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:504px;" ><![endif]-->
<table cellpadding="0" cellspacing="0" style="...">
<tbody>
<tr>
<td
style="direction:ltr;font-size:0px;padding:0 48px 28px;text-align:center;"
>
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:504px;" ><![endif]-->
<table cellpadding="0" cellspacing="0" style="...">
<!-- no flexbox or grid... -->
<!-- just tables in tables in tables... -->
</table>
</td>
</tr>
</tbody>
</table>

<!-- [...] -->

a foretaste of hell (circa 2022, colorized)

So, how then?

MJML to the rescue

Fortunately, MJML was created to save our souls. It’s an awesome framework that abstract most of the pain of writing responsive HTML emails by providing a set of stylable MJML custom elements (buttons, columns, images, etc… transformed to plain HTML once built) that you can reuse to write the markup of your emails.

<mjml>
<mj-body>
<mj-section>
<mj-column>
<mj-image width="100px" src="/logo.png"></mj-image>
<mj-divider></mj-divider>
<mj-text font-family="helvetica">Hello World</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>

To install it from npm, without surprise:

npm i mjml

MJML also provides a way to include external .mjml files in your templates using <mj-include>, which allows you to reuse some markup in several emails.

But as MJML is not a templating language, there are things it doesn’t provide, and that you might need:

  • variables
  • conditionals
  • loops
  • parameterized components

So as long as you have a single email template to write, or that none of your emails needs such templating features, MJML by itself is great, and sufficient. But as soon as you need to go a bit further, you’ll need to pair it with a templating engine, like React.

Using React templating features with MJML

Even if React is mostly known as a reactive view library, it provides all the features you would expect from a templating engine and a composition library.

npm i react react-dom

So let’s say we have to write several email templates that all share the same header, main content, and footer, and that only some text change between them:

1. First, we need components

Let’s start by creating our three components (only Header.tsx is detailed here, but the principle is the same for other components):

components/Header.tsx

export function Header({ title, logo }) {
return (
<mj-section background-color="#fff">
<mj-column>
{logo && <mj-image width="150px" src={logo} />}
<mj-text>{title}</mj-text>
</mj-column>
</mj-section>
);
}

components/MainContent.tsx

export function MainContent({ content, buttonLabel }) {
// ...
}

components/Footer.tsx

export function Footer() {
// ...
}

2. Then, use these components to create our email templates

Each email template is just a React component that reuses the components we just created. Here’s an example for a welcoming email sent to our customer after they just signed up:

WelcomeEmail.tsx

import { Header } from "components/Header";
import { MainContent } from "components/MainContent";
import { Footer } from "components/Footer";

export function WelcomeEmail() {
return (
<mjml>
<mj-body>
<Header
title="Thank you for your inscription and welcome!"
logo="https://example.com/logo.png"
/>

<MainContent
content="We are happy to count you amongst our beloved customers."
buttonLabel="Sign in to access your account"
/>

<Footer />
</mj-body>
</mjml>
);
}

And so on for other email templates, we just need to change the props passed to components!

3. Finally, let’s build all that

As our goal is to build static HTML email templates, we won’t need any of React’s runtime-related features, so we will use ReactDOM’s renderToString function to transform our React templates into MJML templates, and finally to HTML templates.

No need for a complicated build tool here, a good ol’ NodeJS script will do the trick (but written in TypeScript, we’re not savages):

// scripts/build.tsx

import * as React from "react";
import { renderToString } from "react-dom/server";
import mjml2html from "mjml";
import { join, relative } from "node:path";
import { mkdirSync, writeFileSync } from "node:fs";

const DIST_DIR = join(__dirname, "dist");
const TEMPLATES = {
welcome: WelcomeEmail,
// just add other templates here like: <filename>: TemplateComponent
};

// ensure dist dir before outputing our built templates
mkdirSync(DIST_DIR);

for (const name of Object.keys(TEMPLATES)) {
const TemplateComponent = TEMPLATES[name];

// building our React template component to get MJML markup
const mjml = renderToString(<TemplateComponent />);

// then building the MJML markup to get the final HTML email template
const { html } = mjml2html(mjml, { validationLevel: "strict" });

// writing the final HTML template to filesystem
const htmlPath = join(DIST_DIR, `${name}.html`);
writeFileSync(htmlPath, html, { encoding: "utf-8" });
console.debug(`Wrote ${relative(__dirname, htmlPath)}`);
}

As we wrote our templates/components and our build script in TypeScript, we must run it with a tool like tsx.

# install tsx
npm i tsx -D

# run the build script
tsx scripts/build.tsx

4. Optional: Get a nicer development experience

As every project, we might need to iterate a bit while developing, so let’s get a simple but comfortable environment to work with:

  • simple-hot-reload-server: Serve static files from a directory with automatic hot-reload. We'll use it to serve our built templates during local development.
  • concurrently: Run multiple commands concurrently
npm i simple-hot-reload-server concurrently -D

Then, in package.json:

{
// ...
"scripts": {
"prepare": "mkdir -p dist/",
"build": "tsx scripts/build.tsx",
"build:watch": "tsx watch scripts/build.tsx",
"serve": "npm run prepare && hrs dist/",
"dev": "concurrently --kill-others npm:build:watch npm:serve"
}
// ...
}

By running npm run dev, we'll now have automatic hot-reload (upon changes) of our built templates served locally on the 8080 port.

Conclusion

  • MJML is an awesome tool, it abstracts a lot of the pain of writing HTML emails like tables, media queries, and so on
  • It provides an “inclusion” feature, but doesn’t allow to pass parameters, nor to write conditions, loops, etc
  • To be able to use such templating features, you can use React (or any other templating engine) to build your templates as React components
  • It allows you to create reusable and parameterized components that you can reuse across several email templates
  • Using ReactDOM’s renderToString function, and MJML's mjml2html, you can easily transform your React templates into MJML markup, and finally into HTML email templates
  • When working on projects with large emailing needs, you can even consider fully embracing the React+MJML stack using tools like react-mjml


Write dynamic email templates like a breeze with MJML in React was originally published in ekino-france on Medium, where people are continuing the conversation by highlighting and responding to this story.