Remix and React Router
An action function, where the server code and the component sit in the same file.
In Remix, and in React Router 7 in framework mode, anything inside loader or action is stripped from the client bundle. The send goes there. This page works unchanged for both.
Install
npm install @emails.sh/sdk# .env, read by the dev server. The key comes from
# https://emails.sh/dashboard/api-keys.
EMAILSSH_API_KEY=esh_your_key_hereThe route
// app/routes/subscribe.tsx
import { Form, useActionData } from '@remix-run/react';
import { json, type ActionFunctionArgs } from '@remix-run/node';
import { Emailssh } from '@emails.sh/sdk';
export async function action({ request }: ActionFunctionArgs) {
const form = await request.formData();
const address = String(form.get('email') ?? '');
if (!address.includes('@')) return json({ message: 'Enter an email address.' }, { status: 400 });
const mail = new Emailssh({ apiKey: process.env.EMAILSSH_API_KEY });
try {
const { id } = await mail.send({
from: 'Acme <hello@acme.com>',
to: [address],
subject: 'Confirm your subscription',
html: '<p>Click the link in this email to confirm.</p>',
idempotencyKey: `subscribe-${address}`
});
return json({ id });
} catch (err) {
console.error('emails.sh refused the send', err);
return json({ message: 'Could not send that email.' }, { status: 502 });
}
}
export default function Subscribe() {
const data = useActionData<typeof action>();
return (
<Form method="post">
<label htmlFor="email">Email</label>
<input id="email" name="email" type="email" required />
<button type="submit">Subscribe</button>
{data && 'message' in data ? <p>{data.message}</p> : null}
{data && 'id' in data ? <p>Check your inbox.</p> : null}
</Form>
);
}