ReactJS is a client side JavaScript framework. As such, while you can build good-looking contact forms with loads of client-facing functionality, you need to look elsewhere to do something that requires backend functionality such as send an email of add an entry to a database. This is the challenge I will be addressing in this post - how do you build and deploy a ReactJS contact form that will send an email when submitted.
Our toolbox will consist of:
- ReactJS (obviously)
- Axios (to post data)
- Nodemailer (a Node.js package used to send emails via SMTP)
- Netlify (for deploying)
We are going to capture data from our frontend form and post it to a backend url. We will build a Netlify function which will serve as our backend, take the form data we post and use Nodemailer to email the data to the recipient.
It really is as easy as it sounds.
Let's get started...
Front End
First we'll build the front end using ReactJS. To set things up we'll run npx create-react-app contact-form
in our terminal window. This will provide the standard ReactJS app which we'll modify. And then we wait...
...once our react app is installed we run npm start
to run the app in the browser. We open src/App.js
and remove everything between the <header>
tags so our file looks like this:
import React from 'react';
import logo from './logo.svg';
import './App.css';
function App() {
return (
<div className="App">
</div>
);
}
export default App;
While you're at it, get rid of import logo from './logo.svg'
. Ahh, now we have a blank canvas 😌.
Now, create a new file in the src
directory. This will be our contact form module. I'll call mine contact-form.js
, you can call yours whatever you want. The basic structure of a React module is:
import React from 'react'
export default function FunctionName() {
return (
...
)
}
So we can start by building the structure of our contact form. I'm using material-us but again, you can use the CSS framework of you choice. All that matters is that you have a form:
import React from 'react'
import TextField from "@material-ui/core/TextField";
import Button from "@material-ui/core/Button"
import FormControl from "@material-ui/core/FormControl"
export default function Form() {
return (
<>
<FormControl fullWidth={true}>
<TextField required label="Full name" variant="filled" id="full-name" name="name" className="form-field" />
</FormControl>
<FormControl fullWidth={true}>
<TextField required label="Email" id="email" name="email" variant="filled" className="form-field" onChange />
</FormControl>
<FormControl fullWidth={true}>
<TextField required label="Message" variant="filled" name="message" multiline={true} rows="10" />
</FormControl>
<FormControl>
<div style={{padding: 20}}>
<Grid container spacing={2}>
<div className="form-submit">
<Button variant="contained" color="primary">Submit</Button>
</div>
</Grid>
</Grid>
</div>
</FormControl>
)
}
Now we can import the contact form in App.js
. We modify App.js
as follows:
import React from 'react';
import logo from './logo.svg';
import Form from './contactform'
import './App.css';
function App() {
return (
<div className="App">
<Form />
</div>
);
}
export default App;
Capture Form Data
There are a few additions we need to make. First, we need to capture the form data. And what better way to do this than by using react hooks - specifically useState
which we will use to track and update the 'state' of our data in realtime. Modify the first line in contactform.js
to include the useState
hook:
import React, { useState } from 'react'
Next, we instantiate useState
variable. The variable is a two-item array
with the first item being the state we are tracking, and the second item a function used to update that state:
export default function Form() {
const [data, setData] = useState()
return (
...
)
}
Because we need to capture more than one field from the form, we'll setup data
as an object
:
export default function Form() {
const [data, setData] = useState({name: '', email: '', message: '', sent: false, buttonText: 'Submit', err: ''})
return (
...
)
}
As you can see, we do this by simply setting the initial value of useState in object notation. We also setup a few utility items to track the status of our request and provide feedback to the user, namely sent
, buttonText
and err
. More on these later.
Now we need a way to update our data
object. Easy peasy - we setup a function that tracks changes to our form fields:
...
const [data, setData] = useState({name: '', email: '', message: '', sent: false, buttonText: 'Submit', err: ''})
const handleChange = (e) => {
const {name, value} = e.target
setData({
...data,
[name]: value
})
}
...
As its name suggests this function will be called whenever a user changes one of the form fields (i.e. by filling it in). The function uses object destructing to grab the name
and value
attributes of the form field being changed and updates the corresponding value in the data
object.
The last thing we need to do is update the onChange
and value
attributes of our form fields to call this function as the user types:
<FormControl fullWidth={true}>
<TextField required label="Full name" variant="filled" id="full-name" name="name" className="form-field" value={data.name} onChange={handleChange} />
</FormControl>
<FormControl fullWidth={true}>
<TextField required label="Email" id="email" name="email" variant="filled" className="form-field" value={data.email} onChange={handleChange} />
</FormControl>
<FormControl fullWidth={true}>
<TextField required label="Message" variant="filled" name="message" multiline={true} rows="10" value={data.message} onChange={handleChange} />
</FormControl>
<FormControl>
<div className="form-submit">
<Button variant="contained" color="primary">Submit</Button>
</div>
</FormControl>
Handle Form Submissions
We need to setup a function that handles form submissions and we'll call it
const formSubmit = (e) => {
e.preventDefault()
}
We use the preventDefault
function to stop the form redirecting the user to the backend URL which is its default behaviour.
Remember way back when, when I said we need to post
the data to our backend URL? Well that's where Axios comes in - it's a promise-based http client and will serve our needs perfectly. Grab it by running npm i axios
and once it's installed we can finish our submit function:
const formSubmit = (e) => {
e.preventDefault();
setData({
...data,
buttonText: 'Sending...'
})
axios.post('/api/sendmail', data)
.then(res => {
if(res.data.result !=='success') {
setData({
...data,
buttonText: 'Failed to send',
sent: false,
err: 'fail'
})
setTimeout(() => {
resetForm()
}, 6000)
} else {
setData({
...data,
sent: true,
buttonText: 'Sent',
err: 'success'
})
setTimeout(() => {
resetForm();
}, 6000)
}
}).catch( (err) => {
//console.log(err.response.status)
setData({
...data,
buttonText: 'Failed to send',
err: 'fail'
})
})
}
Let's go through what this function does. After preventing the default behaviour of the form, the form sets the buttonText
item of the data
object to 'Sending...'. We will use this to change the text on the submit button and give the user some feedback.
Next, the function perfoms and axios.post
request to the url api/sendmail
which will call our Netlify function when we buid that. If the response is anything but 'success' the button text will be changed to 'Failed to send' and our utility item err
will be set to 'fail' for use later. The form then resets after 6 seconds with the setTimeout
function.
If the response is 'success' then the button text is changed to 'Sent' and err
item changed to 'success'. We then deal with any request-related errors in the same way within the catch
clause.
You'll notice that we reference a resetForm
function. And here it is:
const resetForm = () => {
setData({
name: '',
email: '',
message: '',
sent: false,
buttonText: 'Submit',
err: ''
});
}
This function sets the data
object back to its original state.
We then just need to change the onClick
and value attributes of our button to call the handleSubmit
function and update the button text accordingly:
<Button variant="contained" color="primary" onClick={formSubmit}>{data.buttonText}</Button>
Netlify Functions
Netlify functions allow you to write APIs that give your apps server-side functionality. In our case we are going to write a function that will take our data
object as a post
request and use nodemailer
to send an email to a recipient.
The first thing I would suggest is to install the Netlify CLI by running npm install netlify-cli -g
. This wil help us to test our form. Then we create a directory called functions
in our project root (you don't have to call it 'functions'). In functions
folder, create a file called sendmail.js
. Notice something? Our axios.post
requests posts to api/sendmail
- this is important the post location and the function filename need to be the same.
By this point, Netlify CLI should have installed, so we grab a copy of nodemailer which is a free Node.js module that, in their words, allows 'easy as cake email sending'. Everyone loves cake. Run npm install nodemailer
.
While that's installing we head into our sendmail.js
file and add this code:
const nodemailer = require('nodemailer');
exports.handler = function(event, context, callback) {
let data = JSON.parse(event.body)
let transporter = nodemailer.createTransport({
host:[YOUR SMTP SERVER],
port:[YOUR SMTP SERVER PORT],
auth:{
user:[YOUR SMTP SERVER USERNAME],
pass: [YOUR SMTP SERVER PASSWORD]
}
});
transporter.sendMail({
from: [YOUR SMTP SERVER EMAIL ADDRESS],
to: [RECIPIENT EMAIL ADDRESS],
subject: `Sending with React, Nodemailer and Netlify`,
html: `
<h3>Email from ${data.name} ${data.email}<h3>
<p>${data.message}<p>
`
}, function(error, info) {
if (error) {
callback(error);
} else {
callback(null, {
statusCode: 200,
body: JSON.stringify({
'result': 'success'
})
});
}
});
}
What does this function do, I hear you ask? Netlify functions are all setup in the same way and are documented extensively. In short they export a handler
method and take event
, context
and callback
parameters. In our case we use the event
and callback
parameters which are the equivelant of request
and response
.
First the function parses the request data
object. Next we declare and setup a transporter
variable which holds data related to the SMTP transport we are using. Nodemailer requires the SMTP server, port and authentication information of your chosen SMTP transport. I used Zoho mail which is free, but you can use any provider (Hotmail, Outlook, ...). You can use Gmail which seems a popular choice but there are documented issues with using Gmail so you may want to use another provider.
You can read more about nodemailer SMTP transport here. There is a list of well-known SMTP services that work with nodemailer here.
Back to the function. Once the transporter
variable is setup we use transporter.sendMail(data[, callback])
to configure our message and send the email.
Setting Redirects
We need to do a few final bits to get this up and running. First, we need to create a netlify.toml
file in our project root. This file let's Netlify know what the build configuration is and where any functions are located. In our netlify.toml
file we add two crucial pieces of configuration:
[build]
functions = "functions"
[[redirects]]
from = "/api/*"
to = "/.netlify/functions/:splat"
status = 200
The first is a build command that tells Netlify that our functions are in the functions
directory. Simple.
The second is a redirect that tells Netlify to redirect anything posted to /api/*
should be redirected to our function in the /.netlify/functions/
directory. The :splat
keyword tells Netlify for match anything that follows the asterisk (*), so anything posted to /api/sendmail/
would be redirected to /.netlify/functions/sendmail
, and look sendmail
just happens to be the name of our function file. So our posted data will end up in our function as expected. You can read more about Netlify redirects here
Test Deploy
Because we have installed Netlify CLI, it's easy to test our form by running netlify dev
in our terminal. This will run a local copy of our contact form.
Conclusion
I've added some basic validation to the form as well as react-google-captcha
. You can check out all the code in this repo. For the Netlify function, I modified the code found in this repo. There are a lot of Netlify functions example code snippets here too.
Cover photo credit: Photo by Brett Jordan on Unsplash
Top comments (11)
can get this working on netlify dev but not on the deployed version... Any suggestions. It also does not work when running a standard npm start
Strange - do you have more detail? E.g. any error messages, the Netlify function logs etc? Link to a repo?
I basically just get a 404 post error even when running with npm start. Works perfectly when using netlify dev
Ok, first thing I would suggest is to check your netlify.toml file and make sure the redirect is setup correctly:
[build]
functions = "functions"
[[redirects]]
from = "/api/*"
to = "/.netlify/functions/:splat"
status = 200
But without seeing your code I’m a bit in the dark. So if you post your code on stackoverflow and send me a link to the question, or a direct link to your GitHub repo I can help troubleshoot
I guess the other thing to try is to reference the fully qualified production URL - https://[your app name].netlify.app/api and see if that works
Thank you for this! So far so good, however, I am still yet to deploy my application.
I added another transporter.sendMail() function that sends the email submission to the sender so they can see the message they sent using the contact form, it works perfectly!
It is working correctly with outlook. Sends the email. But when I press submit on the console I see 500 error [HTTP/1.1 500 Internal Server Error 195ms]
lambda response was undefined. check your function code again
Response with status 500 in 163 ms.
Mine works locally no problem but not in production. Getting this error:
createError.js:16 Uncaught (in promise) Error: Network Error
at createError (createError.js:16)
at XMLHttpRequest.handleError (xhr.js:84)
I wanted to see the code, but your link just goes to a repo with a copy of a 'create-react-app' that has not been touched. ???
I also keep getting error while using the JSON.parse function on "exports.handler"
Could you provide more info in the netlify api scripts? I have a similar code but I am stuck trying to post to the backend then to a db using netlify.