Advanced Email Functionality with Node.js, React.js, Nodemailer, and OAuth2 in 2023
Tags: node, react, programming, javascript
Published: 2023-11-01 01:46:40

<p data-pm-slice="1 3 []">Email functionality is an essential part of most modern web applications. Sending emails directly from your app allows password reset flows, order confirmations, contact forms, and more.</p>
In this comprehensive guide, we will walk through integrating email capabilities into a Node.js and React.js app using the following technologies:
<ul>
 	<li><strong>Node.js - Our backend runtime</strong></li>
 	<li><strong>Express.js - Node.js web framework</strong></li>
 	<li><strong>React.js - Frontend UI library</strong></li>
 	<li><strong>Nodemailer - Module for sending emails</strong></li>
 	<li><strong>OAuth2 - For authenticating with Gmail securely</strong></li>
</ul>
We will build an entire email-sending workflow from start to finish, including setting up a Node.js API, authenticating with Gmail via OAuth2, building a React form to capture message data, integrating Nodemailer to construct and send emails, and adding email templates/styling.

<em>Follow this guide to learn </em><a class="attrlink" href="https://blog.learnhub.africa/2023/08/29/how-to-scrap-linkedin-company-data-using-voyager-api/" target="_blank" rel="noreferrer nofollow noopener"><em>How to Scrap LinkedIn Company Data using Voyager Api</em></a><em>.</em>
<h2>Overview &amp; Goals</h2>
By the end of this guide, you will have built and deployed a production-ready email API with React and Node.js.

Some key goals:
<ul>
 	<li>Learn how to connect Node.js to Gmail using OAuth2 for authentication</li>
 	<li>Create reusable email templates with HTML/CSS</li>
 	<li>Build a React form to capture user data for emails</li>
 	<li>Use Nodemailer to construct email messages</li>
 	<li>Add email scheduling/queueing capabilities</li>
 	<li>Deploy the API and frontend on Vercel/Netlify</li>
</ul>
You should have Node.js and npm installed before starting. Some JavaScript knowledge is recommended.

Let's get started!
<blockquote><em>Developers are becoming addicted to drugs. Find out why in </em><a class="attrlink" href="https://blog.learnhub.africa/2023/10/25/tech-and-drugs-the-dark-side-of-tech/" target="_blank" rel="noreferrer nofollow noopener"><em>Tech and Drugs: The Dark Side of Tech</em></a></blockquote>
<h2>Setting up the Node.js Email API</h2>
First, we must create our Node.js server with Express to handle email-sending requests.
<ul>
 	<li><strong>Install Dependencies</strong></li>
</ul>
Create a new directory for the project:
```bash">mkdir node-email-api
cd node-email-api
```
Initialize npm:

<code>npm init -y</code>

Install dependencies:

<code>npm install express nodemailer dotenv</code>
<ul>
 	<li><strong>express</strong> - web framework for Node.js</li>
 	<li><strong>nodemailer</strong> - module for sending emails</li>
 	<li><strong>dotenv</strong> - loads environment variables from the .env file&nbsp;</li>
</ul>
<ul>
 	<li><strong>Create the Express App</strong></li>
</ul>
Open index.js and set the initial Express app:
```javascript
// index.js


const express = require('express');
const nodemailer = require('nodemailer');
require('dotenv').config();
const app = express();


const port = 3000;
app.listen(port, () =&gt; {
  console.log(`Server listening on port ${port}`);  
});
```
This creates the Express app, requires Nodemailer, loads environment variables, and configures the server to listen on port 3000.
<ul>
 	<li><strong>Environment Variables</strong></li>
</ul>
Next, we need to store our credentials securely using environment variables.

Create a .env file in the root of your project:

<code>EMAIL_USER=yourgmailusername</code>

<code>EMAIL_PASS=yourgmailpassword</code>

Replace yourgmailusername and yourgmailpassword with your actual Gmail credentials.

<strong>NOTE</strong>: For added security, you may want to use an <a class="attrlink" href="https://support.google.com/accounts/answer/185833" target="_blank" rel="noreferrer nofollow noopener">App Password</a> instead of your actual Gmail password.

We'll access these values using <a href="https://blog.learnhub.africa/2023/08/29/how-to-scrap-linkedin-company-data-using-voyager-api/" target="_blank" rel="noopener noreferrer nofollow">process.env.EMAIL</a>_USER and <a href="https://blog.learnhub.africa/2023/08/29/how-to-scrap-linkedin-company-data-using-voyager-api/" target="_blank" rel="noopener noreferrer nofollow">process.env.EMAIL</a>_PASS in our code.
<blockquote><em>Do you want to send emails without servers? Find out in this guide, </em><a class="attrlink" href="https://blog.learnhub.africa/2023/10/30/smtp-js-send-email-without-a-server-from-the-browser/" target="_blank" rel="noreferrer nofollow noopener"><em>SMTP.js – Send Email without a Server from the Browser</em></a><em>.</em></blockquote>
<h2>Creating a Development SMTP Server</h2>
When developing and testing our application locally, we don't want actually to send emails to real addresses.

Instead, we'll create a fake SMTP server that captures any messages and prints them to the console instead of sending real emails.

Install <a class="attrlink" href="https://www.npmjs.com/package/nodemailer-stub-transport" target="_blank" rel="noreferrer nofollow noopener">nodemailer-stub-transport</a>:

<code>npm install nodemailer-stub-transport</code>

Then, in index.js, let's create our dev SMTP transport:
```javascript
// Create dev transport

const devTransport = {
  host: "localhost",
  port: 1025,  
  auth: {
    user: process.env.EMAIL_USER,
    pass: process.env.EMAIL_PASS
  }

}
const devAccount = await nodemailer.createTestAccount();
const transporter = nodemailer.createTransport(devTransport);
```
This configures a fake SMTP server running on port 1025 locally. Any emails will be logged to the console rather than sent.

Later, we can switch this to a live transport when deploying our app.
<ul>
 	<li><strong>Sending Test Emails</strong></li>
</ul>
With our dev SMTP server setup, let's test sending our first email.

In index.js, add:
```javascript
// Send email

const info = await transporter.sendMail({
  from: '"App Name" &lt;noreply@example.com&gt;',
  to: 'test@example.com',
  subject: 'Test email',
  text: 'Hello world?',
  html: '&lt;b&gt;Hello world?&lt;/b&gt;'

});
console.log('Message sent: %s', info.messageId);
```
This uses Nodemailer to send a test message.

If we run the server with node index.js, we should see the email contents logged in the terminal:
```javascript
Server listening on port 3000
Accepted mail for delivery: Hello world?
```
Great! Our local dev environment is ready to start building out the API.
<h2>Connecting to Gmail with OAuth2</h2>
Right now, our API can send emails locally but not through an actual Gmail account.

To connect securely, we'll integrate OAuth2 authentication with Gmail API.
<ul>
 	<li><strong>Creating Gmail API Credentials</strong></li>
</ul>
First, we need to create credentials for our app in Gmail:
<ol>
 	<li>Go to <a class="attrlink" href="https://console.cloud.google.com" target="_blank" rel="noreferrer nofollow noopener">Google Cloud Console</a></li>
 	<li>Create a new project</li>
 	<li>Enable the Gmail API for your project</li>
 	<li>Under Credentials, create an <strong>OAuth Client ID</strong></li>
 	<li>Select Web Application and set the redirect URL to <a href="https://blog.learnhub.africa/2023/08/29/how-to-scrap-linkedin-company-data-using-voyager-api/" target="_blank" rel="noopener noreferrer nofollow">https://developers.google.com/oauthplayground</a></li>
 	<li>Grab the Client ID and Client Secret - we'll add these to our .env file shortly.</li>
</ol>
<ul>
 	<li><strong>Installing Googleapis</strong></li>
</ul>
We need the <a class="attrlink" href="https://www.npmjs.com/package/googleapis" target="_blank" rel="noreferrer nofollow noopener">googleapis</a> library to integrate with Google's APIs:

<code>npm install googleapis</code>
<ul>
 	<li><strong>OAuth2 Playground Setup</strong></li>
</ul>
To simplify the OAuth2 flow, we'll use <a class="attrlink" href="https://developers.google.com/oauthplayground/" target="_blank" rel="noreferrer nofollow noopener">OAuth2 Playground</a> to get our refresh token.

In OAuth Playground:
<ol>
 	<li>Click the settings gear -&gt; OAuth2 configuration</li>
 	<li>Check "Use your own OAuth credentials."</li>
 	<li>Enter your Client ID and Client Secret from the API console</li>
 	<li>Close settings</li>
</ol>
This will enable your own custom credentials for the OAuth2 flow.
<ul>
 	<li><strong>Obtaining Refresh Token</strong></li>
</ul>
Now we can get a refresh token:
<ol>
 	<li>In OAuth Playground, select "Gmail API v1" from the list of APIs</li>
 	<li>Click Authorize APIs</li>
 	<li>Grant access to your account</li>
 	<li>Click "Exchange authorization code for tokens."</li>
 	<li>Grab the refresh_token value</li>
</ol>
This is the key we'll use to generate access tokens to call the Gmail API.
<ul>
 	<li><strong>Storing Credentials in .env</strong></li>
</ul>
Update your .env file with the credentials:
<pre><code class="language-javascript">EMAIL_USER=yourgmailusername 
EMAIL_PASS=yourgmailpassword


OAUTH_CLIENT_ID=xxxxxxxxxxx
OAUTH_CLIENT_SECRET=xxxxxxxxxx  
OAUTH_REFRESH_TOKEN=xxxxxxxxxx</code></pre>
Replace the values for OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET, and OAUTH_REFRESH_TOKEN with your credentials.

Never commit this file to source control for security!
<ul>
 	<li><strong>Implementing OAuth2 with Gmail</strong></li>
</ul>
Now, in our code, we can use these credentials to authenticate with the Gmail API.

First, create a new file gmail.js, with the OAuth2 helper functions:
```javascript
// gmail.js
const {google} = require('googleapis');
const OAuth2 = google.auth.OAuth2;

const oauth2Client = new OAuth2(
  process.env.OAUTH_CLIENT_ID, 
  process.env.OAUTH_CLIENT_SECRET, 
  "https://developers.google.com/oauthplayground"
);

oauth2Client.setCredentials({
  refresh_token: process.env.OAUTH_REFRESH_TOKEN
});

const getAccessToken = async () =&gt; {
  return new Promise((resolve, reject) =&gt; {
    oauth2Client.getAccessToken((err, token) =&gt; {
      if (err) {
        reject("Failed to create access token :( " + err);
      }
      resolve(token);
    });
  })
}

module.exports = {
  getAccessToken: getAccessToken
}
```
This creates an OAuth2 client instance and uses the refresh token to generate access tokens.

Inside index.js, import the getAccessToken method:

<code>const {getAccessToken} = require('./gmail');</code>

When we need to call the Gmail API, we can get an access token:

<code>const accessToken = await getAccessToken();</code>

This access token can be used to authenticate requests.
<ul>
 	<li><strong>Creating Gmail Transporter</strong></li>
</ul>
With OAuth2 setup, we can now create a Nodemailer transport for emailing through Gmail.

Inside index.js:
```javascript
// Create live transport

const transporter = nodemailer.createTransport({

  service: 'gmail',

  auth: {

    type: 'OAuth2',

    user: process.env.EMAIL_USER,

    accessToken,

    clientId: process.env.OAUTH_CLIENT_ID,

    clientSecret: process.env.OAUTH_CLIENT_SECRET,

    refreshToken: process.env.OAUTH_REFRESH_TOKEN

  }

});
```
We pass the access token here to authenticate requests.

To send with Gmail, use transporter.sendMail() instead of our dev SMTP transport.

And that's it! Our API can now securely send emails through a Gmail account

<img title="party popper" src="https://paper.dropboxstatic.com/static/img/ace/emoji/1f389.png?version=8.0.0" alt="party popper" align="left" />

Let's recap what we've built so far:
<ul>
 	<li>A Node.js server with Express</li>
 	<li>Environment variables for credentials</li>
 	<li>Local dev SMTP server for testing</li>
 	<li>OAuth2 authentication with the Gmail API</li>
 	<li>Nodemailer transport for sending mail through Gmail</li>
</ul>
Next, we'll build the API routes to handle sending emails based on frontend requests.
<h2>Building the Email API Routes</h2>
Now that we can securely send emails, we must create API endpoints that our React frontend can communicate with.

<strong>Creating Email Templates</strong>

First, let's set up some reusable email templates that we can render with different customer data.

Under templates/ create:
<ul>
 	<li>email-confirmation.html - Order confirmation template</li>
 	<li>password-reset.html - Password reset template</li>
</ul>
For example:
<ul>
 	<li><strong>email-confirmation.html</strong>
```jhtml

<div>
  <h1>Order Confirmation</h1>

  <p>Hi {{name}}, your order has been received!</p>

  <p>Order Details:</p>

  <ul>
    <li>Items: {{items}}</li>
    <li>Total: {{total}}</li>    
  </ul>

  <p>Thank you for shopping with us!</p>
</div>
```
</li>
</ul>
This is a simple confirmation email that we can populate with the customer's name, ordered items, total cost, etc.
<ul>
 	<li><strong>password-reset.html</strong>
```
<div>
  <h1>Password Reset</h1>

  <p>Hi {{name}}, here is your password reset link:</p>

  <p><a href="{{resetUrl}}">Reset Password</a></p>

  <p>Or copy paste this link into your browser:</p>

  <p>{{resetUrl}}</p>
</div> 

```
</li>
</ul>
A template for sending password reset links.

These will be rendered with the customer's data before sending.
<ul>
 	<li><strong>Email Route</strong></li>
</ul>
Now, let's make an endpoint for general email sending:

<strong>routes/email.js</strong>
```javascript
const router = require('express').Router();
const nodemailer = require('nodemailer');

router.post('/', async (req, res) =&gt; {

  try {

    const {to, from, subject, html} = req.body;

    const mailOptions = {
      from,
      to,
      subject, 
      html
    };

    const info = await transporter.sendMail(mailOptions);

    return res.json({ message: 'Email sent successfully' });

  } catch (error) {
    return res.status(500).json({ error: error.message }); 
  }

});

module.exports = router;
```
This takes the email data from the request body and forwards it to Nodemailer.

Import this route in index.js:
```javascript
const emailRoutes = require('./routes/email');
// Use email routes
app.use('/email', emailRoutes);
```
We can now send a general email via a POST to /email.
<ul>
 	<li><strong>Confirmation Email Route</strong></li>
</ul>
Next, let's make an endpoint specifically for order confirmation emails:

<strong>routes/confirm.js</strong>
```javascript
const router = require('express').Router();

const emailTemplate = require('../templates/email-confirmation'); 

router.post('/', async (req, res) =&gt; {

  try {

    const { email, name, items, total } = req.body; 

    const template = emailTemplate
      .replace('{{name}}', name)
      .replace('{{items}}', items)
      .replace('{{total}}', total);

    const mailOptions = {
      from: '"Company Name" &lt;company@email.com&gt;',
      to: email,
      subject: 'Your order confirmation',
      html: template
    };

    await transporter.sendMail(mailOptions);

    return res.json({ message: 'Order confirmation sent!' });

  } catch (error) {
    return res.status(500).json({ error: error.message });
  }

})

module.exports = router;
```
It loads the order confirmation template, replaces the placeholder data, and sends the rendered email.

Import this in index.js as well:
```javascript
const confirmRoutes = require('./routes/confirm');
app.use('/confirm', confirmRoutes); 
```
So when the customer completes an order, the frontend can POST to /confirm with their order data to send the confirmation email.
<ul>
 	<li><strong>Password Reset Route</strong></li>
</ul>
Finally, let's make an endpoint for password reset emails:

<strong>routes/reset.js</strong>
```javascript
const router = require('express').Router();

const emailTemplate = require('../templates/password-reset');

router.post('/', async (req, res) =&gt; {

  try {

    const { email, name, resetUrl } = req.body;

    const template = emailTemplate
      .replace('{{name}}', name)
      .replace('{{resetUrl}}', resetUrl);

    const mailOptions = {
      from: '"Company" &lt;company@email.com&gt;',
      to: email,
      subject: 'Password reset request',
      html: template
    };

    await transporter.sendMail(mailOptions);

    return res.json({ message: 'Password reset email sent!' });

  } catch (error) {
    return res.status(500).json({ error: error.message });
  }

});

module.exports = router;
```
Import into index.js:
```javascript
const resetRoutes = require('./routes/reset');
app.use('/reset', resetRoutes);
```
The frontend can POST to /reset with the user's email and reset link to send password reset instructions.

And that's it for our API! We now have:
<ul>
 	<li>Reusable email templates</li>
 	<li>Routes for sending general emails</li>
 	<li>Confirmation email endpoint</li>
 	<li>Password reset email endpoint</li>
</ul>
Next up, build the React frontend to use these API routes!
<h2>Creating the React Form</h2>
Now that our Node.js email API is finished let's build a React frontend for capturing user data to send emails.

Create a new React project:

<code>npx create-react-app email-frontend</code>

Replace App.js with:
```javascript
import { useState } from 'react';

function App() {

  const [email, setEmail] = useState('');

  const handleSubmit = async (e) =&gt; {
    e.preventDefault();

    // Send email API request
  }

  return (
    &lt;div className="app"&gt;
      &lt;h1&gt;Send Email&lt;/h1&gt;

      &lt;form onSubmit={handleSubmit}&gt;
        &lt;input 
          placeholder="Email address"
          value={email}
          onChange={(e) =&gt; setEmail(e.target.value)} 
        /&gt;

        &lt;button type="submit"&gt;
          Send Test Email
        &lt;/button&gt;
      &lt;/form&gt;
    &lt;/div&gt;
  );
}

export default App;
```
This displays a simple form with an email input. Let's integrate with our API.
<ul>
 	<li><strong>Sending Test Emails</strong></li>
</ul>
First, we'll send a test email when the form is submitted:
```javascript
// Send test email

const res = await fetch('/email', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    to: email,
    from: 'test@example.com', 
    subject: 'Test Email',
    html: '&lt;p&gt;This is a test email from the React app!&lt;/p&gt;'  
  )

});


const data = await res.json();
console.log(data);
```
This makes a POST request to /email with the email contents.

After submitting the form, we should see the test email printed in the Node console.
<ul>
 	<li><strong>Confirmation Email</strong></li>
</ul>
Next, let's send a confirmation email on form submit:
```javascript
const res = await fetch('/confirm', {
  method: 'POST',
  headers: {

    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    email,
    name: 'Test User',
    items: ['Item 1', 'Item 2'], 
    total: '19.99'
  }) 
});

const data = await res.json();
```
This will call the /confirm endpoint, populate the template with the data, and send the confirmation message.

The user should receive the formatted confirmation email after submitting the form.
<ul>
 	<li><strong>Password Reset Email</strong></li>
</ul>
Finally, we can test sending a password reset email:
```javascript
const resetUrl = 'https://example.com/reset';
const res = await fetch('/reset', {

  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    email,
    name: 'Test User',
    resetUrl 
  })
});

const data = await res.json(); 
```
This will send the password reset instructions to the user with the reset URL included.

And that covers the basics of integrating our React form with the Nodemailer API! The user can now trigger emails on submit.
<h1>Connecting to a Database</h1>
Right now, our email templates and routes are hardcoded. For a more robust solution, let's connect to a database to make them dynamic.

We'll use MongoDB as our data store.
<ul>
 	<li><strong>Installing Mongoose</strong></li>
</ul>
First, install Mongoose to interact with MongoDB:

<code>npm install mongoose</code>
<ul>
 	<li><strong>Creating a Database</strong></li>
</ul>
Next, create a free MongoDB database at <a class="attrlink" href="https://www.mongodb.com/atlas/database" target="_blank" rel="noreferrer nofollow noopener">MongoDB Atlas</a>.

Copy the connection URI provided.
<ul>
 	<li><strong>Connecting to MongoDB</strong></li>
</ul>
Inside index.js, connect to the database:
```javascript
const mongoose = require('mongoose');

mongoose.connect(connectionURI, {
  useNewUrlParser: true,
  useUnifiedTopology: true  
});
const db = mongoose.connection;
db.on('error', err =&gt; {
  // error handling
});

db.once('open', () =&gt; {
  // connected
});
```
We're now connected to MongoDB!
<h2>Email Template Model</h2>
Next, create a Mongoose model for email templates:
```javascript
const templateSchema = new mongoose.Schema({
  name: String,
  subject: String,
  body: String
});

const Template = mongoose.model('Template', templateSchema);
```
This defines the schema for a template.
<h2>Saving Templates</h2>
When an admin creates a new template, we can save it:
```javascript
const template = new Template({
  name: 'Confirmation Email',
  subject: 'Order Confirmation',
  body: '&lt;p&gt;Thanks for your order!&lt;/p&gt;' 
});
template.save()
```
The template is now saved to the database.
<h2>Fetching Templates</h2>
To render a template for sending, fetch it from MongoDB:
```javascript
const template = await Template.findOne({ name: 'Confirmation Email' });
// render template.body
```
And that's the basics of integrating a database to store our email templates!

Now, we can easily manage templates without hardcoded files.
<h1>Adding Email Scheduling</h1>
Right now, emails are sent immediately when triggered from our API. To provide more flexibility, let's add the ability to schedule emails to be sent in the future.
<ul>
 	<li>Agenda Library</li>
</ul>
We'll use the Agenda library to schedule jobs (emails) in Node.js:

<code>npm install agenda</code>
<ul>
 	<li><strong>Configuring Agenda</strong></li>
</ul>
In index.js:
```javascript
const Agenda = require('agenda');
const agenda = new Agenda({db: {address: connectionURI}});
agenda.define('send email', async job =&gt; {
  // send email here
});

(async function() {

  // Start scheduler
  await agenda.start();

  // Schedule email for future 
  await agenda.schedule('in 20 minutes', 'send email', {
    to: 'test@example.com',
    subject: 'Scheduled Email',
    body: 'This email was scheduled!'
  });

})()
```
This starts the Agenda scheduler, defines a 'send email' job, and schedules it to run in 20 minutes.
<ul>
 	<li><strong>Queuing Emails</strong></li>
</ul>
Rather than sending immediately, we can now queue emails:
```javascript
await agenda.now('send email', {
  to, 
  subject,
  body
});
```
This will queue the email to be sent by the Agenda job at the specified time.
<ul>
 	<li><strong>Recurring Emails</strong></li>
</ul>
We can also easily schedule recurring reminder emails using cron syntax:
```javascript
agenda.define('reminder', async job =&gt; {
  // send reminder email 
});
agenda.every('3 days', 'reminder', {

  to,

  subject,

  body

})
```
This will send the reminder email every 3 days.

And that covers the basics of adding email scheduling and queues! The API is now much more flexible and performant.

If you find this article thrilling, discover extra thrilling posts like this on <a class="attrlink" href="http://blog.learnhub.africa/" target="_blank" rel="noreferrer nofollow noopener">Learnhub Blog</a>; we write a lot of tech-related topics from <a class="attrlink" href="https://blog.learnhub.africa/category/cloud-computing/" target="_blank" rel="noreferrer nofollow noopener">Cloud computing</a> to <a class="attrlink" href="https://blog.learnhub.africa/category/frontend/" target="_blank" rel="noreferrer nofollow noopener">Frontend Dev</a>, <a class="attrlink" href="https://blog.learnhub.africa/category/security/" target="_blank" rel="noreferrer nofollow noopener">Cybersecurity</a>, <a class="attrlink" href="https://blog.learnhub.africa/category/data-science/" target="_blank" rel="noreferrer nofollow noopener">AI</a>, and <a class="attrlink" href="https://blog.learnhub.africa/category/blockchain/" target="_blank" rel="noreferrer nofollow noopener">Blockchain</a>. Take a look at <a class="attrlink" href="https://blog.learnhub.africa/2023/02/16/how-to-build-offline-web-applications/" target="_blank" rel="noreferrer nofollow noopener">How to Build Offline Web Applications. </a>
<h2><strong>Resource</strong></h2>
<ul>
 	<li><a class="attrlink" href="https://blog.learnhub.africa/2023/01/18/how-to-send-an-email-in-node-js-using-nodemailer/" target="_blank" rel="noreferrer nofollow noopener">How to send an email in node.js using nodemailer</a></li>
 	<li><a class="attrlink" href="https://www.w3schools.com/js/js_arrays.asp" target="_blank" rel="noreferrer nofollow noopener">Javascript Array method</a></li>
 	<li><a class="attrlink" href="https://blog.learnhub.africa/2023/05/09/20-best-javascript-frameworks-for-2023/" target="_blank" rel="noreferrer nofollow noopener">20 Best React JavaScript Frameworks For 2023</a></li>
</ul>