htmlreactjsnext.jsnetlifynetlify-form

How can I use React/Next.js to insert conditional HTML based on element ID (using Netlify forms)?


Apologies if this has been answered somewhere. I've searched and troubleshot for hours to no avail.

First, I have yet to properly learn React, as it is new to me.

I only want to use it for a contact form, and I have been following the tutorial over at FreeCodeCamp's website to do so.

The React version of the form displays fine, but does not submit messages nor does it show the success message.

The HTML-only version of the form (part 1 of the aforementioned tutorial) submits messages fine, so I know I have the form itself set up correctly.

I also tried building the same React app under Next.js (using npx create-next-app instead of npx create-react-app) which somehow solves the problems (I don't know how), but introduces other problems.

The HTML-only version isn't what I want (long story short), but either the React or Next.js versions will work fine, if I can fix the problems of either (or both), so I need help finding the easiest solution here please.

I could be wrong but I believe Netlify forms require the action element, so I'd like to avoid using onSubmit if at all possible.

Using the FreeCodeCamp code, the URL does not display ?success=true upon submitting the form, which I assume is the main culprit (or a symptom of it) behind the issues.

It uses this code in index.js to display form based on an ID in an existing HTML element (necessary because I'm building the app for a static site):

import React from 'react';
import ReactDOM from 'react-dom/client';
import Contact from './contact-form';

const root = ReactDOM.createRoot(document.getElementById('contact-form'));
root.render(
  <React.StrictMode>
    <Contact />
  </React.StrictMode>
);

On the other hand, Next.js uses this code in its _app.js file (which works the same as React's index.js as far as I can tell):

import '../styles/main.css'

function Contact({ Component, pageProps }) {
  return <Component {...pageProps} />
}

export default Contact

The issue with Next.js is that it, by default, is not inserting the HTML into my other HTML file based on an ID. In fact, it renders directly from the contact-form.js file (normally the App.js file but I renamed it). I didn't think this would be hard to change, so I made _app.js look like this:

import '../src/main.css' // for dev preview only
import React, { useEffect } from 'react';
import ReactDOM, { createRoot } from 'react-dom/client';
import Contact from './index';

const root = ReactDOM.createRoot(document.getElementById('contact-form'));
root.render(
  <React.StrictMode>
    <Contact />
  </React.StrictMode>
);

export default Contact

But this code fails because it cannot find "document" (I assume because it hasn't loaded yet). I looked around for solutions and couldn't understand how to implement useEffect but wrapping everything below import and above export with if (typeof window !== "undefined"){} did solve my issue...

And it introduced another one: Next.js now complains about an hydration failure. After a bit of reading, I mostly understand what this means, but I still have no idea how to fix it.

I don't know whether the React version or Next.js version is easier to fix. My preference is to get the React version fixed because I'll be focused on learning React in the near future, but the Next.js version seems easier to fix because it actually works, barring some annoying errors.

I realise it's better to specify which version I need help with, but I don't know if one of them is fundamentally broken over the other and thus not worth trying to fix.

I'd really appreciate it if someone could give me a hand please.

For reference, though it's pretty much the same as in the aforementioned tutorial, here is my code that is being rendered:

import './main.css'; // for dev preview only
import { useState, useEffect } from 'react';

function Contact() {
    const [success, setSuccess] = useState(false);
    
    useEffect(() => {
      if ( window.location.search.includes('success=true') ) {
        setSuccess(true);
      }
    }, []);

  return (
<div className="contact-form">
    <h2 className="grid-item grid-header">Contact Us</h2>
    <form className="grid-item grid-form" name="contact" id="contact-form" method="POST" action="/?success=true" data-netlify="true" data-netlify-recaptcha="true" netlify-honeypot="bot-field">
        <input type="hidden" name="contact" value="contact" />
        {success && (
          <p style={{ color: 'green'}}>
            Sent successfully
          </p>
        )}
        <p id="bot-field">
            <label className="grid-form-label">Do Not Fill</label>
            <input className="grid-form-item" type="text" name="bot-field" />
        </p>
        <p>
            <label className="grid-form-label" htmlFor="name">Name</label>
            <input className="grid-form-item" type="text" id="name" name="name" required />
        </p>
        <p>
            <label className="grid-form-label" htmlFor="email">Email</label>
            <input className="grid-form-item" type="text" id="email" name="email" required />
        </p>
        <p>
            <label className="grid-form-label" htmlFor="subject">Subject</label>
            <input className="grid-form-item" type="text" id="subject" name="subject" required />
        </p>
        <p>
            <label className="grid-form-label" htmlFor="message">Message</label>
            <textarea className="grid-form-text" id="message" name="message" required></textarea>
        </p>
        <div className="captcha" data-netlify-recaptcha="true"></div>
        <p>
            <button className="grid-form-btn" type="submit">Send</button>
        </p>
    </form>
</div>
  );
}

export default Contact;

The code in the other HTML file is simply:

<div id="contact-form-wrapper">
</div>

EDIT: I found a working solution, and have added it as an answer. Part of it was just an oversight on my part. I'm still not completely sure why the original React-only version doesn't work but I will retry it just in case.


Solution

  • I decided to try Netlify's version which moves the POST functionality into a script instead.

    After a lot of trial and error, I successfully integrated the success message. Here is my working code:

    import React from 'react';
    
    const encode = (data) => {
    return Object.keys(data)
        .map(key => encodeURIComponent(key) + "=" + encodeURIComponent(data[key]))
        .join("&");
    }
    
    class ContactForm extends React.Component {
    constructor(props) {
      super(props);
      this.state = { name: "", email: "", subject: "", message: "", success: false };
    }
    
    handleSubmit = e => {
      fetch("/", {
        method: "POST",
        headers: { "Content-Type": "application/x-www-form-urlencoded" },
        body: encode({ "form-name": "contact", ...this.state })
      })
            .then(() =>
                this.setState({
                    name: "",
                    email: "",
                    subject: "",
                    message: "",
                    success: true
                })
            )
        .catch(error => this.setState({ success: false }));
    
      e.preventDefault();
    };
    
    handleChange = e => this.setState({ [e.target.name]: e.target.value });
    
    render() {
        const { name, email, subject, message, success } = this.state;
        return (
                <div className="contact-form">
                    <h2 className="grid-item grid-header">Contact Us</h2>
    <form className="grid-item grid-form" name="contact" onSubmit={this.handleSubmit} data-netlify="true" netlify-honeypot="bot-field">
                        <input type="hidden" name="form-name" value="contact" />
                        {success && (
                          <p style={{ color: 'green'}}>
                            Sent successfully
                          </p>
                        )}
                        <p id="bot-field">
                            <label className="grid-form-label">Do Not Fill</label>
                            <input className="grid-form-item" type="text" name="bot-field" />
                        </p>
                        <p>
                            <label className="grid-form-label" htmlFor="name">Name</label>
                            <input className="grid-form-item" type="text" name="name" value={name} onChange={this.handleChange} required />
                        </p>
                        <p>
                            <label className="grid-form-label" htmlFor="email">Email</label>
                            <input className="grid-form-item" type="text" name="email" value={email} onChange={this.handleChange} required />
                        </p>
                        <p>
                            <label className="grid-form-label" htmlFor="subject">Subject</label>
                            <input className="grid-form-item" type="text" name="subject" value={subject} onChange={this.handleChange} required />
                        </p>
                        <p>
                            <label className="grid-form-label" htmlFor="message">Message</label>
                            <textarea className="grid-form-text" name="message" value={message} onChange={this.handleChange} required></textarea>
                        </p>
                        <p>
                            <button className="grid-form-btn" type="submit">Send</button>
                        </p>
                    </form>
                </div>
            );
        }
    }
    
    export default ContactForm;
    

    But there's something I glossed over in Netlify's documentation which makes this work, and that's the HTML part which is required. A Netlify form requires you to mirror the JavaScript version in the HTML document where it'll be embedded, so it can detect the fields, which is why the HTML-only version worked and the others did not.

    This is the code in my contact.html file:

    <form name="contact" netlify netlify-honeypot="bot-field" hidden>
        <input type="text" name="bot-field" />
        <input type="text" name="name" />
        <input type="email" name="email" />
        <input type="text" name="subject" />
        <textarea name="message"></textarea>
    </form>
    <div id="contact-form-wrapper">
    </div>
    

    Also, to clear the form on submit, each named field is set to "" (blank value) on successful submission, because HTMLFormElement.reset() will also remove the success message without rewriting the code yet again.

    It turns out that Netlify's captcha only works on static HTML-only forms, so you need a custom captcha implementation, else the form breaks. I've edited the code to reflect that.

    You can re-add data-netlify-recaptcha="true" to the form in both JS and HTML parts, plus <div data-netlify-recaptcha="true"></div> in the HTML part if you decide to implement custom ReCAPTCHA 2.