DEV Community

Cover image for ๐Ÿ”ฅ Building an email automation system with React Flow and Resend ๐ŸŽ‰
Nevo David Subscriber for novu

Posted on • Edited on • Originally published at novu.co

๐Ÿ”ฅ Building an email automation system with React Flow and Resend ๐ŸŽ‰

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. ๐Ÿ“

Email


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

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.

ReactFlow


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


Enter fullscreen mode Exit fullscreen mode

Run the code snippet below to install the ReactFlow and Resend packages.



npm install reactflow resend


Enter fullscreen mode Exit fullscreen mode

Finally, installย React Reduxย andย Redux Toolkitย packages to enable us to manage states within the application.



npm install react-redux @reduxjs/toolkit


Enter fullscreen mode Exit fullscreen mode

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>
        </>
    );
}


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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;
};


Enter fullscreen mode Exit fullscreen mode

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;


Enter fullscreen mode Exit fullscreen mode
  • From the code snippet above,
    • We created two states - nodes and edges arrays. The nodes state has a single element representing the initial node in the diagram.
    • The setNodes reducer updates the nodes and edges array. It executes when the user clicks the Add 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.

Resize

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,
    },
});


Enter fullscreen mode Exit fullscreen mode

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>
    );
}


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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' />
        </>
    );
}


Enter fullscreen mode Exit fullscreen mode
  • 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 the setNodes 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 an id 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";


Enter fullscreen mode Exit fullscreen mode

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]
);


Enter fullscreen mode Exit fullscreen mode
  • 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 and useEdgesState 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.

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>
);


Enter fullscreen mode Exit fullscreen mode

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.

Resend

Create an API Key and save it into a .env.local file within your Next.js project.



RESEND_API_KEY=<place_your_API_key>


Enter fullscreen mode Exit fullscreen mode

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});
}


Enter fullscreen mode Exit fullscreen mode

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);
        });
};


Enter fullscreen mode Exit fullscreen mode

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
};


Enter fullscreen mode Exit fullscreen mode

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

Help

Top comments (31)

Collapse
 
sumitsaurabh927 profile image
Sumit Saurabh

Love this article! ๐Ÿš€

Collapse
 
nevodavid profile image
Nevo David

Thank you so much Sumit!

Collapse
 
suede profile image
Yuval

good job, as always

Collapse
 
nevodavid profile image
Nevo David

Thank you so much Yuval!

Collapse
 
matijasos profile image
Matija Sosic

Awesome stuff Nevo - react-flow looks amazing!

Collapse
 
nevodavid profile image
Nevo David

Thank you so much Matija! ๐Ÿ”ฅ

Collapse
 
bukinoshita profile image
Bu Kinoshita

WOW, that's awesome!

Collapse
 
nevodavid profile image
Nevo David

๐Ÿ™‡๐Ÿปโ€โ™‚๏ธ

Collapse
 
gaurbprajapati profile image
gaurbprajapati

Content ๐Ÿ”ฅ

Collapse
 
nevodavid profile image
Nevo David

๐Ÿš€

Collapse
 
leandro_nnz profile image
Leandro Nuรฑez

Awesome. Thanks!

Collapse
 
nevodavid profile image
Nevo David

Thank you LEANDRO!

Collapse
 
mezieb profile image
Okoro chimezie bright

Thanks for sharing

Collapse
 
nevodavid profile image
Nevo David

Thank you for reading!

Collapse
 
abest45 profile image
abest

Love this thank you so much

Collapse
 
nevodavid profile image
Nevo David

Thank you ๐Ÿš€

Collapse
 
costas8 profile image
Costasgk

Keep up the good work!

Collapse
 
nevodavid profile image
Nevo David

Thank you so much Costasgk!

Collapse
 
reconsumeralization profile image
reconsumeralization

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:

RESEND_API_KEY=<place_your_API_key>
Enter fullscreen mode Exit fullscreen mode

For the send.js file (using Bull for job queue):

import { Resend } from "resend";
import Queue from "bull";

const resend = new Resend(process.env.RESEND_API_KEY);

const emailQueue = new Queue('emails', 'redis://127.0.0.1:6379');

emailQueue.process(async (job) => {
  const { email, subject, task } = job.data;
  await resend.emails.send({
    from: "name@yourcompany.dev",
    to: [email],
    subject,
    text: task,
  });
});

export default async function handler(req, res) {
  const { subject, email, tasks } = req.body;
  if (!subject || !tasks || !email) {
    return res.status(400).json({ invalid: true });
  }

  for (const task of tasks) {
    await emailQueue.add({ email, subject, task }, { delay: 600000 });
  }

  return res.status(200).json({ invalid: false });
}
Enter fullscreen mode Exit fullscreen mode

For the pages/index.js file:

const sendEmail = (index) => {
  fetch("/api/send", {
    method: "POST",
    body: JSON.stringify({
      email,
      subject,
      tasks: nodes.map(data => data.value),
    }),
    headers: {
      "Content-Type": "application/json",
    },
  })
    .then((data) => {
      alert(`Sent to processing`);
    })
    .catch((err) => {
      alert(`Encountered an error when message${index} โŒ`);
      console.error(err);
    });
};

const handleSubmit = (e) => {
  e.preventDefault();
  sendEmail();
  setEmail("");
  setSubject("");
};
Enter fullscreen mode Exit fullscreen mode

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.