TL;DR
In this tutorial, you'll learn how to build an email automation system to message people with a sequence of messages every 10 minutes. โฐ
- Build a client diagram representing the flow of emails with React Flow. โฟณ
- Send email according to the flow every 10 minutes with Resend. ๐
Novu - the first open-source notification infrastructure
Just a quick background about us. Novu is an open-source notification infrastructure. We basically help to manage all the product notifications. It can be In-App (the bell icon like you have in the Dev Community - Websockets), Emails, SMSs and so on.
We actually implemented ReactFlow and Resend in our project as well
I would be super happy if you could give us a star! It will help me to make more articles every week ๐
https://github.com/novuhq/novu
ReactFlow to build your flow โ
ReactFlowย is an easy-to-use library for building anything from static diagrams to data visualizations and even complex visual editors. It is highly customizable and provides various in-built features such as dragging nodes around, zooming and panning, selecting multiple nodes and edges, and many more by default.
In this article, you'll learn how to add interactive diagrams to your React apps with ReactFlow and how to send emails withย Resendย by building an email outreach application.
The application accepts various email content via nodes in ReactFlow and sends them as email messages.
Let's set it up ๐ฅ
Here, I'll walk you through installing the package dependencies required for this project; using Next.js v12.
npx create-next-app@12 email-outreach-app
Run the code snippet below to install the ReactFlow and Resend packages.
npm install reactflow resend
Finally, installย React Reduxย andย Redux Toolkitย packages to enable us to manage states within the application.
npm install react-redux @reduxjs/toolkit
Putting the basic page layout ๐
Here, we'll create a form that accepts an email, a subject, and a series of nodes containing the messages you want to send to the recipient. The messages will be sent at an interval of 30 minutes.
First, copy the code snippet below into the pages/index.js
file.
import Head from "next/head";
import { useState } from "react";
export default function Home() {
const [email, setEmail] = useState("");
const [subject, setSubject] = useState("");
const handleSubmit = (e) => {
e.preventDefault();
console.log({ email, subject });
setEmail("");
setSubject("");
};
return (
<>
<Head>
<title>Email Outreach - Resend & ReactFlow</title>
<meta name='description' content='Generated by create next app' />
<meta name='viewport' content='width=device-width, initial-scale=1' />
<link rel='icon' href='/favicon.ico' />
</Head>
<main className='main'>
<header className='header'>
<h1 style={{ marginBottom: "15px" }}>
Email Outreach with ReactFlow and Resend
</h1>
</header>
<form className='form' onSubmit={handleSubmit}>
<label htmlFor='email'>Email</label>
<input
type='email'
name='email'
id='email'
className='input'
value={email}
required
onChange={(e) => setEmail(e.target.value)}
/>
<label htmlFor='subject'>Subject</label>
<input
type='text'
name='subject'
id='subject'
className='input'
value={subject}
required
onChange={(e) => setSubject(e.target.value)}
/>
{/* --- ๐๐ป ReactFlow Component placeholder ๐๐ผ --- */}
<button className='submitBtn'>START AUTOMATION</button>
</form>
</main>
</>
);
}
The code snippet above creates a simple form that accepts the recipient's email address and the subject of the email. In the upcoming section, we'll add the ReactFlow component.
Managing states within the ReactFlow components
Before you import the ReactFlow components, let's set up the state management library - Redux Toolkit.
๐กย PS: You don't need a state management library to use ReactFlow.
We are using Redux to enable us to track the input within the component and update the application's state accordingly. Otherwise,ย you can add ReactFlow components easily.
Therefore, create a redux
folder containing a nodes.js
and a store.js
file.
mkdir redux
cd redux
touch nodes.js store.js
Copy the code snippet below into the redux/nodes.js
file.
import { createSlice } from "@reduxjs/toolkit";
const addNode = (object) => {
const newNode = {
id: `${Number(object.id) + 1}`,
type: "task",
position: { x: 0, y: object.position.y + 120 },
data: { value: "" },
};
return newNode;
};
const addEdge = (object) => {
const newEdge = {
id: `${object.id}->${Number(object.id) + 1}`,
source: `${object.id}`,
target: `${Number(object.id) + 1}`,
};
return newEdge;
};
The code snippet above contains two functions that accept an object (the last element in the nodes array) and returns another object containing the values above.
Next, add the code snippet below the functions - in the same file.
//below the functions (within the same file)
//---- ๐๐ป functions ๐๐ผ---
export const nodeSlice = createSlice({
name: "nodes",
initialState: {
nodes: [
{
id: "1",
type: "task",
position: { x: 0, y: 0 },
data: { value: "" },
},
],
edges: [],
},
reducers: {
setNodes: (state, action) => {
let nodes = state.nodes;
state.nodes = [...state.nodes, addNode(nodes[nodes.length - 1])];
state.edges = [...state.edges, addEdge(nodes[nodes.length - 1])];
},
updateNodeValue: (state, action) => {
let nodes = [...state.nodes];
let objectIndex = nodes.findIndex((obj) => obj.id === action.payload.id);
if (objectIndex !== -1) {
state.nodes[objectIndex] = {
...nodes[objectIndex],
data: { value: action.payload.value },
};
}
},
},
});
// Action creators are generated for each case reducer function
export const { setNodes, updateNodeValue } = nodeSlice.actions;
export default nodeSlice.reducer;
- From the code snippet above,
- We created two states -
nodes
andedges
arrays. Thenodes
state has a single element representing the initial node in the diagram. - The
setNodes
reducer updates thenodes
andedges
array. It executes when the user clicks theAdd button
within each diagram node. - The
updateNodeValue
reducer tracks the input within each node of the diagram and updates the right node with its new value.
- We created two states -
Add the node reducer to the store.js
file.
import { configureStore } from "@reduxjs/toolkit";
import nodeReducer from "./nodes";
export const store = configureStore({
reducer: {
nodes: nodeReducer,
},
});
Finally, make the store available to the whole application by updating the _app.js
file.
import { store } from "../redux/store";
import "../styles/globals.css";
import { Provider } from "react-redux";
export default function App({ Component, pageProps }) {
return (
<Provider store={store}>
<Component {...pageProps} />
</Provider>
);
}
Congratulations! You've set up the states required for the diagram. Next, let's add it to the app.
Adding the ReactFlow components
Since we are using a custom component for each node in the diagram, create a components
folder containing a Task.js
file.
mkdir components
cd components
touch Task.js
Copy the code below into the Task.js
file. The Task component represents each node in the diagram.
import { useState } from "react";
import { Handle, Position } from "reactflow";
import { useSelector, useDispatch } from "react-redux";
import { setNodes, updateNodeValue } from "../redux/nodes";
export default function Task({ id }) {
const initialNodes = useSelector((state) => state.nodes.nodes);
const [value, setValue] = useState("");
const dispatch = useDispatch();
return (
<>
<Handle type='target' position={Position.Top} />
<div
style={{
padding: "10px",
backgroundColor: "#F5F5F5",
borderRadius: "5px",
}}
>
<input
className='textInput'
type='text'
required
onChange={(e) => {
setValue(e.target.value);
dispatch(updateNodeValue({ id, value: e.target.value }));
}}
value={value}
/>
{Number(id) === initialNodes.length && (
<button onClick={() => dispatch(setNodes())} className='addBtn'>
ADD NODE
</button>
)}
</div>
<Handle type='source' position={Position.Bottom} id='a' />
</>
);
}
- From the code snippet above,
- Theย Handle componentsย rendered at the top and bottom connect each node to another. It has a type prop that determines whether the node is a source or target.
- The
Add Node
button triggers thesetNodes
reducer. - When a user updates the content within the input field, the
updateNodeValue
reducer is also triggered to update the selected note with the input value. - Each node in the diagram has a
data
and anid
props containing the details of that node.
Next, add the following imports to the pages/index.js
file.
import { useState, useCallback, useMemo, useEffect } from "react";
import ReactFlow, {
useNodesState,
useEdgesState,
getIncomers,
getOutgoers,
addEdge,
getConnectedEdges,
} from "reactflow";
import "reactflow/dist/style.css";
import Task from "../components/Task";
import { useSelector } from "react-redux";
Add the code snippet below within the Home component on the pages/index.js
file.
const initialNodes = useSelector((state) => state.nodes.nodes);
const initialEdges = useSelector((state) => state.nodes.edges);
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const nodeTypes = useMemo(() => ({ task: Task }), []);
useEffect(() => {
setNodes(initialNodes);
setEdges(initialEdges);
}, [initialNodes, setNodes, initialEdges, setEdges]);
const onConnect = useCallback(
(params) => setEdges((eds) => addEdge(params, eds)),
[setEdges]
);
- From the code snippet above,
- The nodes and edges from the Redux state are set as the nodes and edges for the diagram using the
useNodesState
anduseEdgesState
hooks provided by ReactFlow. - The
nodeTypes
variable enables us to customise each node.Task
is our custom component. - The
onConnect
function executes when you add a new node. - The
useEffect
hook runs when there are changes in the edges and the nodes.
- The nodes and edges from the Redux state are set as the nodes and edges for the diagram using the
Finally, add the ReactFlow
component to the user interface as done below.
return (
<form>
{/*---๐๐ป other form elements ๐๐ผ---*/}
<div style={{ height: "60vh", width: "100%", marginTop: "20px" }}>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
nodeTypes={nodeTypes}
/>
</div>
<button className='submitBtn'>START AUTOMATION</button>
</form>
);
Congratulations, you've successfully added the diagram to your application.
Resend.com to send your emails ๐
In this section, you'll learn how to send emails with Resend by sending the inputs in each node to the email provided on the form.
Resendย is an email API that enables you to send texts, attachments, and email templates easily. With Resend, you can build, test, and deliver transactional emails at scale.
One of its best features is that your messages don't end up in the recipient's spam box but in the recipient's inbox.
We've already installed Resend at the beginning of this tutorial. Therefore, go to theย Signup page and create an account.
Create an API Key and save it into a .env.local
file within your Next.js project.
RESEND_API_KEY=<place_your_API_key>
Next, create a send.js
file within the pages/api
folder and copy the code below into the file.
//๐๐ป within the send.js file
import { Resend } from "resend";
// initiate the resend instance
const resend = new Resend(process.env.RESEND_API_KEY);
const timer = (time) => {
return new Promise((res) => {
setTimeout(() => res(true), time);
});
}
export default async function handler(req, res) {
const { subject, email, tasks } = req.body;
if (!subject || !tasks || !email) {
res.status(400).json({invalid: true});
}
for (const task of tasks) {
await resend.emails.send({
from: "name@yourcompany.dev",
to: [email],
subject,
text: task,
});
// Wait 10 minutes
await timer(600000);
}
res.status(200).json({invalid: false});
}
The code snippet above receives the subject, recipient, and email content from the request and sends an email to the recipient viaย Resend.
Please be advice that there is a delay of 10 minutes between emails.
This will not be possible to be deployed to Vercel as their free package support a maximum of 10 seconds per request.
You can absolutly test it on your local machine.
In production, such a thing would need to go into a queue that sends the email every X amount of time.
Add the following functions within the pages/index.js
file.
const sendEmail = (index) => {
fetch("/api/send", {
method: "POST",
body: JSON.stringify({
email,
subject,
tasks: nodes.map(data => data.value), // map all nodes to a string array
}),
headers: {
"Content-Type": "application/json",
},
})
.then((data) => {
alert(`Sent to processing`);
})
.catch((err) => {
alert(`Encountered an error when message${index} โ`);
console.error(err);
});
};
The functions above loop through the nodes in the ReactFlow diagram and sends an email containing the node's value to the recipient at intervals.
Finally, execute the function when a user submits the form.
const handleSubmit = (e) => {
e.preventDefault();
sendEmail();//๐๐ผ Send to server
setEmail(""); // Reset the input
setSubject(""); // Reset the input
};
Let's wrap it up ๐
So far, you've learned how to add interactive diagrams to your application with ReactFlow and send emails with Resend.
ReactFlow is aย popular open-source libraryย that enables us to build interactive and customizable flowcharts and diagrams. If you want to build an application that requires drag-and-drop functionality and customizable graphical UI elements, you should consider using ReactFlow.
The source code for this tutorial is available here: ย https://github.com/novuhq/blog/tree/main/email-outreach-with-reactflow-and-resend
Thank you for reading! ๐
Help me out!
If you feel like this article helped you understand email automation better! I would be super happy if you could give us a star! And let me also know in the comments โค๏ธ
https://github.com/novuhq/novu
Top comments (31)
Love this article! ๐
Thank you so much Sumit!
good job, as always
Thank you so much Yuval!
Awesome stuff Nevo - react-flow looks amazing!
Thank you so much Matija! ๐ฅ
WOW, that's awesome!
๐๐ปโโ๏ธ
Content ๐ฅ
๐
Awesome. Thanks!
Thank you LEANDRO!
Thanks for sharing
Thank you for reading!
Love this thank you so much
Thank you ๐
Keep up the good work!
Thank you so much Costasgk!
Certainly! Here's an updated version of the code that includes a job queue (such as Bull) to handle the delayed email sending.
.env.local file:
For the send.js file (using Bull for job queue):
For the pages/index.js file:
This updated code snippet utilizes a Redis-backed job queue (Bull) to handle the 10-minute delay between email sends. In this way, it bypasses the request handling limitations of serverless platforms like Vercel, and it can be efficiently managed and scaled.
Bull will take care of scheduling the email jobs with a 10-minute delay, and you won't have to worry about request timeouts. Make sure you have Redis running on your server, and you might need to include Bull and Redis as dependencies in your project.
Now, you have a more scalable solution that's suitable for production. Enjoy your emailing without the horror of manual delays! ๐๐ผ
Some comments may only be visible to logged-in visitors. Sign in to view all comments.