react-email-handlebars
v1.3.0
Published
Handlebars template support for React Email and JSX Email. Write templates once in React, generate Handlebars for dynamic rendering.
Downloads
16
Maintainers
Readme
react-email-handlebars
Handlebars template support for React Email and JSX Email
A library that bridges the gap between React-based email templates and Handlebars templating. Write your email templates once using React components, and generate Handlebars-compatible templates for dynamic content rendering.
Features
- Dual Runtime Support: Works seamlessly with both React Email and JSX Email
- Type-Safe: Full TypeScript support with Zod schema validation
- Preview Mode: Preview your emails with sample data during development
- Build Mode: Generate Handlebars templates for production use
- Conditional Rendering:
IfandUnlesscomponents for conditional content - List Rendering:
Eachcomponent for iterating over data arrays
Installation
npm install react-email-handlebars zod
# or
pnpm add react-email-handlebars zod
# or
yarn add react-email-handlebars zodNote:
zodis required when using theEachcomponent for schema validation.
Peer Dependencies
This library requires React 19 or higher:
npm install react@^19.0.0 react-dom@^19.0.0Usage
Runtime Provider
Wrap your email template with RuntimeProvider to specify the runtime mode:
preview: Renders with preview data (for development)build: Generates Handlebars template syntax (for production)
If Component
Conditionally render content based on a Handlebars path.
React Email Example
import { Body, Html, Text } from "@react-email/components";
import { If, RuntimeProvider } from "react-email-handlebars";
export default function WelcomeEmail() {
return (
<RuntimeProvider value="build">
<Html>
<Body>
<If
conditionPath="user.isAdmin"
previewCondition={false}
then={<Text>You are an admin</Text>}
else={<Text>You are not an admin</Text>}
/>
</Body>
</Html>
</RuntimeProvider>
);
}Generated Handlebars Template:
{{#if user.isAdmin}}
You are an admin
{{else}}
You are not an admin
{{/if}}JSX Email Example
import { Body, Html, Text } from "jsx-email";
import { If, RuntimeProvider } from "react-email-handlebars";
export const Template = () => (
<RuntimeProvider value="preview">
<Html>
<Body>
<If
conditionPath="user.isPremium"
previewCondition={true}
then={<Text>Premium Features Unlocked!</Text>}
else={<Text>Upgrade to Premium</Text>}
/>
</Body>
</Html>
</RuntimeProvider>
);Generated Handlebars Template:
{{#if user.isPremium}}
Premium Features Unlocked!
{{else}}
Upgrade to Premium
{{/if}}Unless Component
Inverse of If component. Renders content when a condition is falsy.
React Email Example
import { Body, Html, Text } from "@react-email/components";
import { Unless, RuntimeProvider } from "react-email-handlebars";
export default function PaymentReminderEmail() {
return (
<RuntimeProvider value="build">
<Html>
<Body>
<Unless
conditionPath="user.hasPaid"
previewCondition={false}
then={
<Text>Please make a payment to continue using our services.</Text>
}
else={<Text>Thank you for your payment!</Text>}
/>
</Body>
</Html>
</RuntimeProvider>
);
}Generated Handlebars Template:
{{#unless user.hasPaid}}
Please make a payment to continue using our services.
{{else}}
Thank you for your payment!
{{/unless}}JSX Email Example
import { Body, Html, Text } from "jsx-email";
import { Unless, RuntimeProvider } from "react-email-handlebars";
export const Template = () => (
<RuntimeProvider value="preview">
<Html>
<Body>
<Unless
conditionPath="user.isVerified"
previewCondition={false}
then={<Text>Please verify your email address.</Text>}
/>
</Body>
</Html>
</RuntimeProvider>
);Generated Handlebars Template:
{{#unless user.isVerified}}
Please verify your email address.
{{/unless}}Each Component
Iterate over arrays and render items with type safety.
React Email Example
import { Body, Html, Text, Section } from "@react-email/components";
import { Each, RuntimeProvider } from "react-email-handlebars";
import { z } from "zod";
const userSchema = z.object({
name: z.string(),
email: z.string(),
});
type User = z.infer<typeof userSchema>;
export default function UserListEmail() {
const previewData: User[] = [
{ name: "John Doe", email: "[email protected]" },
{ name: "Jane Smith", email: "[email protected]" },
];
return (
<RuntimeProvider value="build">
<Html>
<Body>
<Section>
<Text>User List:</Text>
<Each
previewData={previewData}
each="users"
schema={userSchema}
renderItem={(user) => (
<div key={user.email}>
<Text>Name: {user.name}</Text>
<Text>Email: {user.email}</Text>
</div>
)}
/>
</Section>
</Body>
</Html>
</RuntimeProvider>
);
}Generated Handlebars Template:
{{#each users}}
<div>
<p>Name: {{name}}</p>
<p>Email: {{email}}</p>
</div>
{{/each}}JSX Email Example
import { Body, Html, Text } from "jsx-email";
import { Each, RuntimeProvider } from "react-email-handlebars";
import { z } from "zod";
const productSchema = z.object({
name: z.string(),
price: z.number(),
});
export const Template = () => {
const previewData = [
{ name: "Product A", price: 29.99 },
{ name: "Product B", price: 49.99 },
];
return (
<RuntimeProvider value="preview">
<Html>
<Body>
<Each
previewData={previewData}
each="products"
schema={productSchema}
renderItem={(product) => (
<Text key={product.name}>
{product.name}: ${product.price}
</Text>
)}
/>
</Body>
</Html>
</RuntimeProvider>
);
};Generated Handlebars Template:
{{#each products}}
<p>
{{name}}: ${{price}}
</p>
{{/each}}Nested Object Support
The Each component supports nested objects in Zod schemas. You can access deep properties using standard dot notation or object destructuring.
const UserSchema = z.object({
id: z.string(),
info: z.object({
name: z.string(),
contact: z.object({
email: z.string(),
}),
}),
});
// ...
<Each
each="users"
schema={UserSchema}
previewData={previewUsers}
renderItem={(user) => (
<Text key={user.id}>
{user.info.name} - {user.info.contact.email}
</Text>
)}
/>;Generated Handlebars Template:
{{#each users}}
<p>
{{info.name}}
-
{{info.contact.email}}
</p>
{{/each}}API Reference
RuntimeProvider
Props:
value:"preview" | "build"- Runtime mode
If
Props:
conditionPath:string- Handlebars path for the conditionpreviewCondition:boolean- Condition value for preview modethen:ReactNode- Content to render when condition is trueelse?:ReactNode- Optional content to render when condition is false
Unless
Props:
conditionPath:string- Handlebars path for the conditionpreviewCondition:boolean- Condition value for preview modethen:ReactNode- Content to render when condition is falseelse?:ReactNode- Optional content to render when condition is true
Each
Props:
previewData:TItem[]- Array of items for preview modeeach:string- Handlebars path for the arrayschema:z.ZodSchema<TItem>- Zod schema defining item structurerenderItem:(item: TItem) => ReactNode- Function to render each item
Development
Setup
pnpm installBuild
pnpm run buildWatch Mode
pnpm run devRun Examples
React Email Example
cd examples/react-email
pnpm install
pnpm run devVisit http://localhost:3000 to see the examples.
JSX Email Example
cd examples/jsx-email
pnpm install
pnpm run devTesting
This library uses @rstest/core for testing. Tests cover both preview and build modes for all components.
pnpm testTest Structure
tests/If.test.tsx: Verifies conditional rendering logic and Handlebars syntax generation.tests/Unless.test.tsx: Verifies inverse conditional logic and Handlebars syntax generation.tests/Each.test.tsx: Verifies list iteration, empty states, and schema validation.
How It Works
The library provides two runtime modes:
Preview Mode (
runtime="preview"):- Uses the provided
previewDataandpreviewCondition - Renders actual React components
- Perfect for development and testing
- Uses the provided
Build Mode (
runtime="build"):- Generates Handlebars template syntax
- Uses Zod schemas to create placeholder variables
- Outputs template strings ready for production use
The runtime context is managed via React Context API, ensuring compatibility with both React Email and JSX Email environments.
License
MIT
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
