DEV Community

Cover image for Function Component React Form Submission on Netlify
Alexa Gamil
Alexa Gamil

Posted on • Edited on

Function Component React Form Submission on Netlify

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
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

We would then call the useState() like so:

const [formData, setFormData] = useState({
        name: "",
        email: "",
        message: ""
    })
Enter fullscreen mode Exit fullscreen mode

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} />
Enter fullscreen mode Exit fullscreen mode

And here is the code for the handleChange()

const handleChange = e => {
  const { name, value } = e.target
  setFormData({...formData, 
            [name]: value
        })
    }
Enter fullscreen mode Exit fullscreen mode

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
    }
Enter fullscreen mode Exit fullscreen mode

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>}
Enter fullscreen mode Exit fullscreen mode

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();
  }
Enter fullscreen mode Exit fullscreen mode

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 the validate function which will take formData as a parameter
  • we will also set isSubmitted to true
  • we are preventing default

Let's go ahead add the onSubmit event to our form

<form onSubmit={handleSubmit}>
Enter fullscreen mode Exit fullscreen mode

Step 4.2 Handle Submit (useEffect)

Next we are importing the useEffect hook from react

import {useState, useEffect} from 'react'
Enter fullscreen mode Exit fullscreen mode

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])
Enter fullscreen mode Exit fullscreen mode

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("&");
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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;
  }
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
ineeader profile image
Inee Ader 🐌

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!

Collapse
 
imckain profile image
Ian McKain

Thanks for the post Alexa! I've been looking all over for solutions. I am getting a 404 on the fetch request. Any thoughts?

Collapse
 
gamil91 profile image
Alexa Gamil

Hi Ian! May I ask, where are you getting the 404? When you try to go to your deployed site?

Collapse
 
imckain profile image
Ian McKain

Hi Alexa, thanks for the response. I receive a 404 on my form submission in my browser console.

Thread Thread
 
gamil91 profile image
Alexa Gamil

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!