I recently built my first version of my portfolio and deployed it on Netlify. I included a simple contact form for anyone who wants to reach me. In this blog, I will share how I used the built-in form handling that comes with deploying through Netlify.
I’m going to start this blog assuming that you already have a react-app created and you already ran (npm start or yarn start) and build my form from scratch. You’ll also need to make a Netlify account using the Git provider that is hosting your source code. It is free and it gives you 100 form submissions/month.
I think that’s enough for the intro, let’s get to coding!
Step 1 - Create a Form Function
I will be creating 3 input fields. One for name
, email
, and message
. This is my starter code.
import React from 'react'
const Form = () => {
return (
<div >
<h1> Sample Form </h1>
<form >
<div className="form-inputs">
<label htmlFor="name" className="form-label">
Name
</label>
<input
type="text"
name="name"
id="name"
className="form-input"/>
</div>
<div className="form-inputs">
<label htmlFor="email" className="form-label">
Email
</label>
<input
type="text"
name="email"
id="email"
className="form-input"/>
</div>
<div className="form-inputs">
<label htmlFor="message" className="form-label">
Message
</label>
<textarea
name="message"
id="message"
className="form-input"/>
</div>
<button type="submit" className="form-input-btn">
Send
</button>
</form>
<div>
)
}
export default Form
I have a div
that’s holding my form
. Each input
is wrapped inside its own div
. Make sure the htmlFor
attribute on the label
and the name
attribute on the input
match. For message, we are using textarea
for multi-line text input. And of course, we have a button
with the type=”submit”
Step 2 - Stateful Function Component
We want to make this form stateful so that we could validate the form for errors before submitting it. We don’t want an inbox full of blank messages. We need to import useState()
hook from react
.
If you’re not familiar with the useState()
, it allows us to create a “state variable” in a function component and it returns the current state
and a function
that updates that specific state. More info here
import {useState} from 'react'
We would then call the useState()
like so:
const [formData, setFormData] = useState({
name: "",
email: "",
message: ""
})
We are declaring a “state variable” called formData
. The argument to the useState()
is the initial state of our variable. In this case, our initial state is an object with the keys matching our input names, each pointing to a value of an empty string. setFormData
will be the function
that will update formData.
We can now add two attributes to our inputs:
- value={formData.[key in the obj that matches the name]}
- an onChange event={handleChange}
<input
type="text"
name="name"
id="name"
className="form-input"
value={formData.name}
onChange={handleChange}/>
<input
type="email"
name="email"
id="email"
className="form-input"
value={formData.email}
onChange={handleChange}/>
<textarea
name="message"
id="message"
className="form-input"
value={formData.message}
onChange={handleChange} />
And here is the code for the handleChange()
const handleChange = e => {
const { name, value } = e.target
setFormData({...formData,
[name]: value
})
}
As you can see, we’re destructuring e.target
and assigning the name
and value
to variables and then were calling setFormData()
to update the state using those variables.
And now we have a stateful react function component! Go ahead and console.log(formData)
anywhere in the function before return and type away in your inputs.
Step 3 Creating Validations
If you didn’t code your form from scratch and you got your form from sites like react-bootstrap, you might not have to do this. Some forms might come with form validations already. Anyway moving on!
Were going to call useState
again and declare a variable called errors
with the initial state of an empty object. And I also wrote a function called validate
const [errors, setErrors] = useState({})
const validate = (formData) => {
let formErrors = {}
if(!formData.name){
formErrors.name = "Name required"
}
if(!formData.email){
formErrors.email = "Email required"
}
if(!formData.message){
formErrors.message = "Message is required"
}
return formErrors
}
This function takes in our formData
variable as a parameter. Then we declare another empty object inside called, formErrors
. We then have conditions that check if each key
in formData
is pointing to an empty string. If it is, this means that our input field is empty then we would add a key-value pair to our formErrors
object. The key
is the name
of the input and the value
is the exact “error message”. I hope that makes sense lol. Validate returns our formErrors
object.
Also under each input field, we are putting this code.
<input
type="text"
name="name"
id="name"
className="form-input"
value={formData.name}
onChange={handleChange}/>
{errors.name && <p>{errors.name}</p>}
<input
type="email"
name="email"
id="email"
className="form-input"
value={formData.email}
onChange={handleChange}/>
{errors.email && <p>{errors.email}</p>}
<textarea
name="message"
id="message"
className="form-input"
value={formData.message}
onChange={handleChange} />
{errors.message && <p>{errors.message}</p>}
The &&
is a shortcut that if errors.name
exists then return this p
tag, which will be rendering our error message.
Now that we have this setup, I’ll explain what this is for in the next step.
Step 4.1 Handle Submit
This time we’ll declare a variable called isSubmitted
with the initial state set to false
.
const [isSubmitted, setIsSubmitted] = useState(false)
const handleSubmit = e => {
setErrors(validate(formData))
setIsSubmitted(true)
e.preventDefault();
}
As you can see from above, I also built our handleSubmit
function.
The function does three things:
- it will call the
setErrors()
and pass in the return value of thevalidate
function which will takeformData
as a parameter- we will also set
isSubmitted
totrue
- we are preventing default
Let's go ahead add the onSubmit event to our form
<form onSubmit={handleSubmit}>
Step 4.2 Handle Submit (useEffect)
Next we are importing the useEffect
hook from react
import {useState, useEffect} from 'react'
If you are not familiar with the useEffect()
, it is basically equivalent to componentDidMount()
and componentDidUpdate()
combined together (and in some cases where it’s needed componentWillUnmount()
)
We will call useEffect
to do our POST request.
useEffect(() => {
if(Object.keys(errors).length === 0 && isSubmitted){
fetch("/", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: encode({ "form-name": "contact-form", ...formData })
})
.then(() => alert("Success!"))
.then(() => setIsSubmitted(false))
.then(() => setFormData({name: "", email: "", message: ""}))
.catch(error => alert(error))}
}, [errors, formData, isSubmitted])
From the code above, we passed a function to useEffect()
, this function could be referred to as the “effect”. Inside that function is a condition to check if the errors
object is empty AND isSubmmitted
set to true. If it is, do the fetch request.
You probably noticed the body: encode({})
- this is a function provided from the Netlify docs.
Above the useEffect()
, copy and paste this exact code.
const encode = (data) => {
return Object.keys(data)
.map(key => encodeURIComponent(key) + "=" +
encodeURIComponent(data[key]))
.join("&");
What could vary here is the “contact-form”
and the ...formData.
I will tackle the “contact-form”
in a bit. For the formData
, all we’re doing here is using the spread operator
. We are then passing it to the encode
function and the return value will be the body
of the POST request.
This is followed by a “Success” alert
, setting back isSubmitted
to false
, and clearing out the input fields by setting the formData
to empty strings.
useEffect()
will apply the effect (which is the function passed) every render. In our case, we want it to skip applying effect unless the errors
obj and isSubmitted
state were changed. Basically when someone presses the submit button because handleSubmit
is where these variables change.
This is why we are passing an array as our second argument to useEffect()
. When our component re-renders, useEffect()
compares those variables to the previous variables from the last rerender. If the variables match it will skip the effect, if they don’t then React will run the effect. Essentially in class components, we would write a componentDidUpdate()
and a comparison with prevProps
or prevState
. More info on useEffect here
Step 5 Helping the Netlify bots
The Netlify bots that look for the netlify attribute only know how to parse HTML. So to give them a little help we are including this code inside our index.html (This file is inside the public directory, just above the src directory.) I put it just below the opening <body>
tag
<body>
<form name="contact-form" netlify netlify-honeypot="bot-field" hidden>
<input type="text" name="name" />
<input type="email" name="email" />
<textarea name="message"></textarea>
</form>
Remember the contact-form
from earlier? The form name has to match what was being encoded in our POST request. In our case it is called “contact-form” You can call it something else, but they just have to match. Otherwise, this won’t work. The second part is that all the names
for the inputs
and textareas
also have to match. In case you’re wondering, labels are not required in this case since this is a hidden form.
To recap, this is my whole code for the form function.
import React from 'react'
import {useState, useEffect} from 'react'
import './form.css'
const Form = () => {
const [formData, setFormData] = useState({
name: "",
email: "",
message: ""
})
const handleChange = e => {
const { name, value } = e.target
setFormData({
...formData,
[name]: value
})
}
const [errors, setErrors] = useState({})
const validate = (formData) => {
let formErrors = {}
if(!formData.name){
formErrors.name = "Name required"
}
if(!formData.email){
formErrors.email = "Email required"
}
if(!formData.message){
formErrors.message = "Message is required"
}
return formErrors
}
const [isSubmitted, setIsSubmitted] = useState(false)
const handleSubmit = e => {
setErrors(validate(formData))
setIsSubmitted(true)
e.preventDefault();
}
const encode = (data) => {
return Object.keys(data)
.map(key => encodeURIComponent(key) + "=" + encodeURIComponent(data[key]))
.join("&");
}
useEffect(() => {
if(Object.keys(errors).length === 0 && isSubmitted){
fetch("/", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: encode({ "form-name": "contact-form", ...formData })
})
.then(() => alert("Success!"))
.then(() => setIsSubmitted(false))
.then(() => setFormData({name: "", email: "", message: ""}))
.catch(error => alert(error))
}
}, [errors, formData, isSubmitted])
// console.log(errors, formData)
return (
<div >
<h1> Sample Form </h1>
<form onSubmit={handleSubmit}>
<div className="form-inputs">
<label htmlFor="name" className="form-label">
Name
</label>
<input
type="text"
name="name"
id="name"
className="form-input"
value={formData.name}
onChange={handleChange}/>
{errors.name && <p>{errors.name}</p>}
</div>
<div className="form-inputs">
<label htmlFor="email" className="form-label">
Email
</label>
<input
type="email"
name="email"
id="email"
className="form-input"
value={formData.email}
onChange={handleChange}/>
{errors.email && <p>{errors.email}</p>}
</div>
<div className="form-inputs">
<label htmlFor="message" className="form-label">
Message
</label>
<textarea
name="message"
id="message"
className="form-input"
value={formData.message} onChange={handleChange} />
{errors.message && <p>{errors.message}</p>}
</div>
<button type="submit" className="form-input-btn">
Send
</button>
</form>
</div>
)
}
export default Form
This is my optional CSS I added. Not anything fancy!
// form.css
.form-inputs p {
font-size: 0.8rem;
margin-top: 0.5rem;
color: #f00e0e;
}
.form-label {
display: inline-block;
font-size: 0.9rem;
margin-bottom: 6px;
}
Step 6 Deploy on Netlify
Let's git add .
git commit -m "form done"
and git push
Below is a 1 minute video of how I deployed this simple form app on Netlify.
Know that there are tons of other ways to go about this, there are sites like Email.js and Pageclip.co that get the job done. I decided to do it through Netlify because I'm already deploying my portfolio there and I wanted it all in one spot. It was also my first time using Netlify and I love how user-friendly it is. I hope this blog was helpful!
Here is the link to the docs Netlify
Top comments (5)
this is SOOOOO helpful, thanks for doing this! I have been using PageClip for form submissions, but I want to learn this and use it on my portfolio site too! What a great walkthrough!
Thanks for the post Alexa! I've been looking all over for solutions. I am getting a 404 on the fetch request. Any thoughts?
Hi Ian! May I ask, where are you getting the 404? When you try to go to your deployed site?
Hi Alexa, thanks for the response. I receive a 404 on my form submission in my browser console.
I'm not sure what could be causing the 404. I didn't run into that bug. The only thing that I could think of is the form name on the index.html is not matching the form name that's encoded in the body of your POST request. Also, I'm not sure if you found this already answers.netlify.com/t/404-error-wh...
Hope that helps!