< WebDevRef />

Contact form ft. Formspree API and useForm hook

Sep 3, 2021

#react

7 min read

last updated: Sep 18, 2021

A quick ping

Email, Linkedin, Twitter - so, you've got these contact points on your portfolio. Won't it be great if a visitor can drop a quick message to you? Maybe a recruiter interested in having a word or someone impressed by your site would like to let you know.

That is what we'll be building today- a quick contact form using the seamless form-to-email API by Formspree, and a custom useForm hook for form validation.

At the Formspree end

  • Register with them.
  • Create a new project
  • Create a new form under that project with the email where you'll like to receive the form data
  • On the form details page, under Integration section, keep a note āœ of your form's endpoint, something like: https://formspree.io/f/abcdexyz

At the React end

The final form of our form šŸ˜

final_form.png

Note: This form is part of my personal portfolio. So the form attributes and CSS might look intimidating šŸ˜…. Please customize as you like.

Ā 

Form validation options:

With web forms, the possibilities of form structure and validation mechanics could be endless. šŸš€ Developing forms could be overwhelming. šŸ¤Æ

Keeping it simple, here are a few possibilities:

But I came across Anurag's Portfolio where he used custom hook and since I've never created a custom hook before, thought it would be a fun learning experience.

So this hook here is a pieced-together version of Anurag's hook and Formspree's React Form docs.

šŸŽThere's a bonus at the end. So do read throughšŸ˜‰

Ā 

The custom useForm hook

Let's build it piece by piece. šŸ‘·ā€ā™€ļø

šŸŒæ The shell

// we'll be using axios to make a request to Formspree API // npm install axios import axios from "axios"; import { useState } from "react"; const useForm = () => { // below pieces will fit here }; export default useForm;

šŸŒæ Form data (inputs)

// intial form data state const defaultFormData = { name: "", email: "", message: "", }; const [formData, setFormData] = useState(defaultFormData); // tracking and storing the field errors as key-value pairs const [errors, setErrors] = useState({}); // handle onChange event for the input fields const handleInput = (e) => { const { name, value } = e.target; setFormData({ ...formData, [name]: value }); };

šŸŒæ The regex (against which we'll validate those fields)

// someone much smarter than me wrote these šŸ˜… const regexValidations = { email: /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/, name: /^[a-zA-Z\s]*[^\s]$/gim, message: /^[\w\d][^<>/\\&]*$/gim, };

šŸŒæ Field validations

const validateInputs = () => { // generating an array from the "keys" of the formData object const inputs = Object.keys(formData); // looping through each field and running the validation checks inputs.forEach((input) => { if (!formData[input]) { // empty field check errors[input] = `Your ${input} is required`; } else if (!regexValidations[input].test(formData[input])) { // testing against the corresponding regex errors[input] = `Please enter a valid ${input}`; } else { // empty value otherwise errors[input] = ""; } }); setErrors((prevState) => ({ ...prevState, ...errors })); // generate array using the values of the error object and check for empty("") strings, since Boolean("") returns false // if length === 0, return true(i.e. proceed with form submission), else false(i.e. prevent form submission and render validation errors) return Object.values(errors).filter(Boolean).length === 0; };

Object.filter(Boolean), passes each item to Boolean(), which coerces them to true or false (depending on whether its truthy or falsy), and we keep the true ones. Credit: Michael Uloth's article

šŸŒæ Render validations errors (if any) for respective fields

const renderErrorMsg = (field) => { if (errors[field]) return <p className="error-msg">{errors[field]}</p>; };

formspree_form_validation_errors.png

šŸŒæ Handle Form Submission

const handleFormSubmit = (e) => { e.preventDefault(); if (!validateInputs()) { // to prevent form from submitting if errors exists return; } // if no errors then, try submitting the form setServerState({ submitting: true }); axios({ method: "POST", url: `https://formspree.io/f/${your-form-endpoint}`, data: formData, }) .then((res) => handleServerResponse(true, "Thank you for the message")) .catch((res) => handleServerResponse(false, res.response.data.error)); };

šŸŒæ The return object

// we'll import these in our form component return { formData, handleInput, handleFormSubmit, errors, serverState, renderErrorMsg, };

Ā 

Form component

// destructuring the object returned from useForm hook const { formData, handleInput, handleFormSubmit, serverState, renderErrorMsg, } = useForm(); <form className="quick-message-form" onSubmit={handleFormSubmit} noValidate // to prevent browser from doing default validations when submitting the form > <label htmlFor="name">Name: </label> <input type="text" id="name" name="name" placeholder="Bond, James Bond" value={formData.name} onChange={handleInput} aria-label="name" /> {renderErrorMsg("name")} <label htmlFor="email">Email: </label> <input type="text" id="email" name="email" placeholder="bond007@mi6.com" value={formData.email} onChange={handleInput} aria-label="email" /> {renderErrorMsg("email")} <label htmlFor="message">Message: </label> <textarea name="message" id="message" cols="30" rows="5" placeholder="Can I have one Vesper Martini, please? Shaken, not stirred." name="message" value={formData.message} onChange={handleInput} aria-label="message" ></textarea> {renderErrorMsg("message")} <button type="submit" id="dispatch-btn" // prevent user from clicking the button while form is submitting disabled={serverState.submitting} aria-label="form submit button" > Dispatch </button> </form>

Ā 

Component Style

  • Global styles(https://github.com/sanjibdey104/portfolio-v1/blob/main/styles/globalStyles.js)
  • Form Styles

Ā 

You must be wondering: "Hey, how do we know whether the form was submitted successfully or not"

šŸ”„ Here's the bonus:

Custom Toast Component for those successful or failed server responses. Place this after the form component.

<> // form component here // given that we have the status we pass the serverState as prop {serverState.status && <ToastMessage serverState={serverState} />} </>

formspree_form_success_toast.png

import React, { useEffect, useState } from "react"; import styled from "styled-components"; const StyledToastMessage = styled.div` width: 15rem; height: 2rem; // positioned relative to the page position: fixed; top: 50%; right: 1rem; display: flex; align-items: center; justify-content: center; border-radius: 0.5rem; padding: 0.5rem; text-align: center; font-size: 0.85rem; font-weight: 500; color: white; /* initially the component is off the screen*/ transform: translateX(120%); transition: transform 200ms ease-in-out; &.error-response { background-color: #ff3333; } &.success-response { background-color: #5cb85c; } &#show { transform: translateX(0); } `; const ToastMessage = ({ serverState }) => { // handling toast state const [showToast, setShowToast] = useState(false); useEffect(() => { setShowToast(true); // the toast disappears after 3 sec const toastTimeout = setTimeout(() => { setShowToast(false); }, 3000); return () => { clearTimeout(toastTimeout); }; }, []); return ( <StyledToastMessage id={showToast ? "show" : null} className={ serverState.status.isSubmitSuccessful ? "success-response" : "error-response" } > // populate with success or error message {serverState.status.msg} </StyledToastMessage> ); }; export default ToastMessage;

Quite a long read, but I hope you liked it. Go ahead, implement it in your own project and share if you find it helpful.

Have a good one. Take care. Bye. šŸ‘‹

Go to all posts...