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