Aman Explains

Settings

From stateful to stateless: my approach to building forms in React app

November 10, 2024• ☕️☕️ 8 min read

I haven’t been using React state much lately when it comes to building forms of any size. The HTML and CSS have advanced so much that we can rely on them to build a fully functional form that delivers a great user experience. You need to use semantic elements and should be familiar with HTML form APIs.

I have written a separate article that goes in detail explaning what thoese APIs are and why building your forms using the language of least power creates an inclusive experience for your users. I’ll recommend reading that article as it will help you understand the power of HTML and CSS.

Table of Contents


Uncontrolled vs Controlled components

Form elements have their own states. The value updates when the user interacts with them. There’s no need to pass the value prop to them, or the onChange event handler. These components’ value is not controlled by React and thus are called uncontrolled components.

const UncontrolledInput = () => (
  <div>
    <label htmlFor="firstname"></label>
    <input type="text" name="firstname" id="firstname" />
  </div>
);

On the contrary, when you pass the value state variable along with the onChange event handler, which internally updates the value state variable, the component becomes a controlled component. If you don’t pass the onChange handler, the user won’t see any updates. React is in charge here of updating the state instead of the form element itself.

const ControlledInput = () => {
  const [inputValue, setInputValue] = useState("");

  const handleOnChange = (event) => {
    // The setter is responsible for updating 
    // the inputValue here.
    setInputValue(event.target.value);
  };

  return (
    <div>
      <label htmlFor="firstname"></label>
      <input
        type="text"
        name="firstname"
        id="firstname"
        value={inputValue}
        onChange={handleOnChange}
      />
    </div>
  );
};

Unless your form fields depends on the values of other sibling fields, you might need to rely on some state variable up in the tree. As you need a way to rerender your form, that’s where React comes in. For example, based on the option selected in a select field, you might need to filter options shown in another select field. This is pure case of reacting to a user selection.

Elements like checkboxes have their own pseudo class :checked that can be used to check their state. We can then use it to hide/show sibling elements via sibling combinator Selectors or via the the legendary :has pseudo class.

Note that you can still pass in an onChange handler to your Uncontrolled components that reacts to any input internal state change. This can be handy if you want to update a ref based on the input value.

Form validation and styling

Using react state variables does give you more control and helps you perform some validation checks or format the input as the user is typing. You might want to style the required fields differently when they are empty, or stop the form submission if it’s not valid.

What if you can do the same using HTML and CSS only? These two languages are least powerful than JS, and as per the rule of least powerful language, they are my go-to choice for building forms these days.

HTML form elements have myriad of APIs to check for validation and formatting. The required attribute is one of them, along with maxlength and minlength. You can also use the pattern attribute to check for a specific pattern. Please check the HTML input attributes for more details.

When it comes to styling, you can use well-supported CSS features like :invalid, or more appropriate the :user-valid and :user-invalid pseudo classes. The :has is one of my favorite one that helps you pull off some of advanced styling easily which were not possible before. You don’t have to rely on JavaScript (or React state variables) to achieve any of this.

The following example demonstrates form validation and styling using an HTML and CSS only approach.

<style>
  input:focus:user-valid {
      outline: 2px solid green;
  }
  input:focus:user-invalid {
      outline: 2px solid red;
  }
</style>
<form method="POST" action="/">
  <label>
    Email: <input type="email" required pattern=".+@example\.com" />
  </label>
  <label>
    Password: <input type="password" required />
  </label>
  <button>Log in</button>
</form>

Notice that we are using the newly available :user-valid and :user-invalid pseudo classes. These pseudo classes help you style the form element after the user has interacted with it, instead of eagerly applying the styles via the old :invalid and :valid pseudo classes, which might confuse the user. Check the live exmaple in my previous “do you even need JavaScript for your basic HTML forms?” article where these style were applied immediately.

You can take it one step further, and show an alert/error feedback that the form is invalid with the help of :has pseudo class.

<style>
  input:focus:user-valid {
      outline: 2px solid green;
  }
  input:focus:user-invalid {
      outline: 2px solid red;
  }
  div[role="alert"] {
    color: tomato;
    display: none;
  }
  form:has(:user-invalid) div[role="alert"] {
    display: block; 
  }
</style>
<form method="POST" action="/">
  <label>
    Email: <input type="email" required pattern=".+@example\.com" />
  </label>
  <label>
    Password: <input type="password" required />
  </label>
  <button>Log in</button>
  <div role="alert">Form is invalid.</div>
</form>
Live Preview

Our form doesn’t need any stateful code that relies on React anymore. It’s still providing a great user experience. Form won’t submit if it’s not valid. The browser will throw in some popups to alert the user about the invalid fields.

Another benefit is that this form will work even if JavaScript is disabled. This is a perfect example of inclusive experience.

Form submission

You can use the form’s onSubmit event handler to capture the form data. Inside the handler, you can either grab individual form element value via event.target.elementName.value or event.target.elements.elementName.value. Additionally, you can also use the FormData API to capture individual form element value or all form element values. Make sure to have a unique name for your fields though.

Let’s look at the following example.

const CustomForm = () => {
  const handleSubmit = (event) => {
    // Prevent default form submission
    event.preventDefault();

    // 1. Grab individual form element value
    const email = event.target.email.value;
    const password = event.target.password.value;

    // 2. Use FormData API to capture individual form element value
    const formData = new FormData(event.target);
    const emailValue = formData.get("email");
    const passwordValue = formData.get("password");

    // 3. Use FormData API to capture all form element values
    const data = Object.fromEntries(formData);

    console.log({ data }); // Will print the form data
    // You can send a POST request to your backend here
  };

  return (
    <form onSubmit={handleSubmit} method="POST" action="/">
      <label>
        Email:{" "}
        <input type="email" name="email" required pattern=".+@example\.com" />
      </label>
      <label>
        Password: <input type="password" name="password" required />
      </label>
      <div role="alert">Form is invalid.</div>
      <button>Log in</button>
    </form>
  );
};

Fill in some valid values to the form below and hit submit. It should log the form values in the console.

Live Preview

Conclusion

I have fallen in love with this approach as it’s stateless (no React state management), lightweight and inclusive. It allows you to build a fully functional form that delivers a great user experience without writing lots of stateful code. Addtionally, there are no rerendering issues since there are no state updates to react to.

This article was not meant to convince you to avoid the React state approach. It was to share my own preferred strategy that I have been using lately to think less about managing form’s state in React. Next time you have a form to build, stop for a moment, and think about how you can build it using form APIs and CSS only. I am sure you will end up removing these unnecassry lines of React code from your form and appreciate the simplicity.

I am keen to know your thoughts on how you are building your forms. Please leave a comment below.

Resources


Amandeep Singh

Written by Amandeep Singh. Developer @  Avarni  Sydney. Tech enthusiast and a pragmatic programmer.