Creating type safe emails with mjml-react and Typescript

I love MJML and I love Typescript. Creating emails without any dynamic content is an easy task with MJML. But when you are adding any dynamic content you have to start using some sort of templating engine like Handlebars or Mustache. But using these engines removes all type safety! Now you could create your own types and hope for the best when building these emails, but this makes debugging and maintaining these emails very hard.

Installing the depedencies

Luckily the folks at Wix created the mjml-react library. Lets start by installing the library and the necessary types by running the following command:

npm install mjml-react @types/mjml-react

Creating a default layout

Next, we have to create our e-mail template. Because we are now using react to create our template we can componentize our email templates very easily. The first thing I do is to create a base layout we can use in all our templates. The example below renders the MjmlHead component in which we can define all our default styles and attributes following the MJML documentation

interface Props {
  children: ReactNode;
}

export function Layout(props: Props) {
  return (
    <Mjml>
      <MjmlHead>{/* Add any default head attributes here */}</MjmlHead>
      <MjmlBody>{props.children}</MjmlBody>
    </Mjml>
  );
}

Creating the email template

With the default layout ready and all our base styles defined, we can start building the template. The props you are defining here are the props we are going to infer from the component when we are sending our email.

interface Props {
  name: string;
  url: string;
}

export function HelloWorld(props: Props) {
  return (
    <Layout>
      <MjmlSection>
        <MjmlColumn>
          <MjmlText>Hello {props.name}!</MjmlText>
          <MjmlButton href={props.url}>Go to page!</MjmlButton>
        </MjmlColumn>
      </MjmlSection>
    </Layout>
  );
}

Rendering and sending the email

To render our email we need to create a small helper function that takes in the component and the needed props. By inferring the type of the props from the component we have created full type safety without the need to manually define any types.

import { render } from "mjml-react";
import { ComponentProps, createElement, JSXElementConstructor } from "react";

export async function renderEmail<T extends JSXElementConstructor<any>>(
  component: T,
  props: ComponentProps<T>
) {
  const email = createElement(component, props);
  const { html, errors } = render(email, {
    minify: false,
  });

  if (errors.length) {
    throw new Error(errors[0].formattedMessage);
  }

  return html;
}

With the helper function created we can now call it from anywhere and pass the resulting html into a module like Nodemailer

// This passes the type check because our props match
const html = await renderEmail(HelloWorld, {
  name: "User",
  url: "https://google.com",
});

// This fails the type check because we are missing a prop
const html = await renderEmail(HelloWorld, {
  name: "User",
});

And thats it! Enjoy creating type safe emails with React and MJML!

© 2024 Ruben Jansen. All rights reserved.