maantje/react-email
Bring React Email to Laravel: build email templates as React components and render them server-side for use with Laravel Mailables. Combines JSX component ergonomics with Laravel’s mail workflow for maintainable, reusable transactional emails.
Install the PHP side.
composer require maantje/react-email
php artisan vendor:publish --tag="react-email-config"
The config file is where the Node binary path, working directory (where your package.json lives), and rendering timeout are wired. Read it before writing any email — most setup failures trace back to these three values.
Install the JS side. You need React and the React Email components in the project the package points at:
npm i react @react-email/components
Keep this node_modules present in every environment that will send mail (including production and queue workers), not just local.
Create your first component. Put .tsx templates where your package.json / tsconfig expects them, e.g. resources/js/emails/Welcome.tsx:
import { Html, Head, Body, Container, Text, Button } from '@react-email/components';
type Props = { name: string; url: string };
export default function Welcome({ name, url }: Props) {
return (
<Html><Head /><Body>
<Container>
<Text>Hi {name},</Text>
<Button href={url}>Get started</Button>
</Container>
</Body></Html>
);
}
Send it from a Mailable. The package exposes a mailable/helper that hands a component path plus props to the Node renderer. Confirm the exact method names against the README for your pinned version — the surface is small but has shifted across releases:
class WelcomeEmail extends ReactEmailMailable
{
public function __construct(public User $user) {}
public function build()
{
return $this->subject('Welcome aboard')
->react('emails/Welcome', [
'name' => $this->user->name,
'url' => route('dashboard'),
]);
}
}
First use case to ship: transactional mail only — password reset, welcome, receipts. Leave marketing/bulk campaigns on Blade until you've watched the renderer behave in production for a release cycle.
<Html>/<Body> as your layout component and nest per-template bodies. Create Emails/Layout.tsx once, export it, and import it in every template. This mirrors Blade layouts and is the single biggest quality-of-life win.Date objects crossing the bridge), and money formatting on the PHP side so the template stays presentational.Mail::fake + assertSee). Whatever the mechanism, build a php artisan tinker one-liner that renders a component with fixture props so you can iterate on a .tsx file without sending mail.implements ShouldQueue works exactly as with Blade mailables. The renderer invocation happens in the queue worker process, so the worker must have Node, the correct working directory, and node_modules — a common "works in web, fails in queue" cause.Notification::toMail() → ->view() won't help; instead return a MailMessage that points at the React mailable, or compose the mailable into the notification's toMail() as a chained MailMessage. Test this path specifically — it's the easiest place to lose props.__('mail.welcome.greeting', ['name' => …])) and pass the resolved strings as props. Rendering with the queue worker's locale is the trap you avoid here.php:8.3-fpm without Node, sending will fail at runtime. Bake Node + npm ci --omit=dev into your image, or pre-render to static HTML and store it.package.json pins the JS runtime. Bump them together in a PR and render every template in CI. With ~50 stars and 0 dependents, treat any version bump as a change to be verified, not trusted.tsconfig paths. The renderer resolves imports from its own entrypoint, so @/components/... aliases silently fail. Use relative imports inside templates, or set the renderer's working directory to the same root as your build.useEffect, useState, and data fetching inside the template will not run (or will throw). Everything is props in, HTML out.alt and avoid critical content in images.ReactEmailService so a future swap (or the package going unmaintained) is a one-file change.How can I help you explore Laravel packages today?