Is accessing DOM elements in "read-only" mode without Refs a React anti-pattern?

Viewed 28

I can't figure out if Refs are necessary if my intention is just to check what's inside of the DOM elements, but not "writing" in them in any way. I have this code in my React application, in which I have a EmailJS form that lets the user send a email to me. I wanted to put a check to make sure that the user would fill each and every input of the form. To do such a thing, the idea was to access, after its submission, the form stored in the DOM to check whether it was filled or not:

Contacts.js

// import statements

export default function Contacts() 
{
    let confirmation;

    const [toSend, setToSend] = useState({
        from_name: '',
        to_name: '',
        message: '',
        reply_to: '',
        confirm: null
    });

    const validateAndSend = () =>
    {
        let isValid = true;
        let stop = false;

        const elementList = document.querySelectorAll("input"); 
        elementList.forEach(  /* Controllo se sono stati compilati tutti i campi del form */
            (e) => {
                if (e.value === ("") && stop === false)  /* Stop è per stampare solo un alert in caso di più campi vuoti */
              {
                alert("You must fill every field of the form!");
                isValid = false;
                stop = true;
              }
            }
        );

        if (isValid === true)
        {
            send(   
                'service_ID',  /* SERVICE ID del servizio su EmailJS.com */
                'template_ID',  /* TEMPLATE ID del template in uso su EmailJS.com */
                toSend,
                'USER ID') /*  USER ID su EmailJS.com */ 
            .then( (response) => {
                console.log("EmailJS server response:", response.status, response.text);
            })
            .catch( (err) => {
                console.log('Error sending email: ', err);
            });

            confirmation = (<p id="confirm_message">Email sent successfully to website's administrator. You will be contacted at the email address you've just provided.</p>) ;
            setToSend((prevState) => { return {...prevState, confirm: true }});
        };

    };

    const handleChange = (e) =>
    {
        setToSend({ ...toSend, [e.target.name]: e.target.value })
    };


    return (
        <div className="Contacts">
            <NavBar />
            <h1>Contact us!</h1>
            { confirmation }
            <form id="contacts-form" onSubmit={(event) => { event.preventDefault(); } }>
                <input type="text" name="from_name" placeholder="from name" value={toSend.from_name} onChange={handleChange} />
                <input type="text" name="to_name" placeholder="to name" value={toSend.to_name} onChange={handleChange} />
                <input type="text" name="message" placeholder="Your message" value={toSend.message} onChange={handleChange} />
                <input type="text" name="reply_to" placeholder="Your email" value={toSend.reply_to} onChange={handleChange} />

                <button type='submit' onClick={ validateAndSend }>Send</button>
            </form>
            <CustomFooter position="stay_fixed" />
        </div>
    );
}

So, the main question is: do I really need to call useRef and access the form with it even tho I don't have any intention to manually edit the DOM ?

Side problem: after the form is submitted, the paragraph stored in { confirmation } does not get displayed.

1 Answers

Any case where you can use refs or other methods is preferred above querying elements from the DOM. One of the exceptions being selecting an element outside of the React root element that is not prone to be removed.

In your case you don't need any Refs either as you can use the Event object from the submit event to extract everything you need from the form. One of the methods is by using a FormData constructor to extract key-value pairs from the form.

Like in your own example, loop over the values and check if they are empty.

If all values are filled in, convert the FormData object to an actual object and send your data.


I also took the liberty to tinker a bit with your component. I feel that using uncontrolled inputs is the way to go. That just leaves a single state isConfirmed to check if the form has been sent successfully. Based on that state, render your confirmation message.

const [isConfirmed, setIsConfirmed] = useState(false);

const validateAndSend = async (event) => {
  event.preventDefault();

  const formData = new FormData(event.target);
  for (const value of formData.values()) {
    if (value === '') {
      alert("You must fill every field of the form!");
      return; // Stop the function.
    }
  }

  // Convert iterable object to actual object.
  const toSend = Object.fromEntries([...formData]);

  try {
    const response = await send(
      'service_ID', /* SERVICE ID del servizio su EmailJS.com */
      'template_ID', /* TEMPLATE ID del template in uso su EmailJS.com */
      toSend,
      'USER ID' /*  USER ID su EmailJS.com */
    );

    console.log("EmailJS server response:", response.status, response.text);
    setIsConfirmed(true);
  } catch (err) {
    console.log('Error sending email: ', err);
  }
};
return (
  <div className="Contacts">
    <NavBar />
    <h1>Contact us!</h1>

    {isConfirmed && (
      <p id="confirm_message"> Email sent successfully to website 's administrator. You will be contacted at the email address you've just provided.</p>
    )}

    <form id="contacts-form" onSubmit={validateAndSend}>
      <input type="text" name="from_name" placeholder="from name" />
      <input type="text" name="to_name" placeholder="to name" />
      <input type="text" name="message" placeholder="Your message" />
      <input type="text" name="reply_to" placeholder="Your email" />

      <button type='submit'>Send</button>
    </form>

    <CustomFooter position="stay_fixed" />
  </div>
);
Related