loading...
Cover image for ⛓ Uncontrolled form validation with React

⛓ Uncontrolled form validation with React

bluebill1049 profile image Bill ・3 min read

When we working with form validation, most of us would be familiar with libraries such as Formik and Redux-form. Both are popular among the community and built with Controlled Components.

What is Controlled Component?

React is driving the internal state of itself. Each input interaction or change will trigger the React's Component life cycle. The benefit of having that is:

Every state mutation will have an associated handler function. This makes it straightforward to modify or validate user input.

This feature is great for handling forms validation. However, there is a hidden cost. If you run the following code and pay attention to the developer console;

function Test() {
  const [numberOfGuests, setNumberOfGuests] = useState();
  console.log('rendering...');

  return (
    <form onSubmit={() => console.log(numberOfGuests)}>
      <input
        name="numberOfGuests"
        value={numberOfGuests}
        onChange={setNumberOfGuests} />
    </form>
  );
}

You should see console.log repeating 'rendering...' in the dev console each time as you type. Obviously, the form is getting re-rendered each time. I guess with simple use case it wouldn't cause much of issue. Let's try to implement something which is more close to a real-world example.

function Test() {
  const [numberOfGuests, setNumberOfGuests] = useState();
  expensiveCalculation(numberOfGuests); // Will block thread
  console.log('rendering...');

  return (
    <form onSubmit={() => console.log(numberOfGuests)}>
      <input
        name="numberOfGuests"
        value={numberOfGuests}
        onChange={setNumberOfGuests} />
    </form>
  );
}

It's pretty much the same code, except this time each render will execute an expensive function before render. (let's assume it will do some heavy calculation and blocking the main thread) hmmm... now we have an issue because user interaction can be potentially interrupted by that. As a matter of fact, this type of scenario did give me a headache in terms of form performance optimization.

Solution

Off course there are solutions on the table, you can use a memorize function to prevent execute the function on each render. An example below:

function Test() {
  const [numberOfGuests, setNumberOfGuests] = useState();
  // The following function will be memoried with argument and avoid recalculation
  const memoizedValue = useMemo(() => computeExpensiveValue(numberOfGuests), [numberOfGuests]);

  return (
    <form onSubmit={() => console.log(numberOfGuests)}>
      <input
        name="numberOfGuests"
        value={numberOfGuests}
        onChange={setNumberOfGuests} />
    </form>
  );
}

However, we actually have another option to skip re-render the form when user typing.

Uncontrolled Components

What's Uncontrolled Component?

In most cases, we recommend using controlled components to implement forms. In a controlled component, form data is handled by a React component. The alternative is uncontrolled components, where form data is handled by the DOM itself.

This means if you are going to build uncontrolled form and you will be working on methods to handle the DOM and form interaction. Let's try an example with that then.

function Test() {
  const numberOfGuests = useRef();
  expensiveCalculation(this.state.numberOfGuests);

  return (
    <form onSubmit={() => console.log(numberOfGuests.current.value)}>
      <input
        name="numberOfGuests"
        ref={numberOfGuests}
        value={numberOfGuests} />
    </form>
  );
}

By leveraging uncontrolled component, we exposed with the following benefits:

  1. User interaction no longer triggers re-render on change.
  2. Potential less code to write.
  3. Access to input's ref gives you the power to do extra things, such as focusing on an error field.

I guess one quick question will pop up in your head, what if I want to listen for input change? Well now you are the driver of the inputs, you can handle that by native DOM event. (it's all just javascript) example below:

function Test() {
  const numberOfGuests = useRef();
  const handleChange = (e) => console.log(e.target.value)

  useEffect(() => {
    numberOfGuests.current.addEventListener('input', handleChange);
    return () => numberOfGuests.current.removeEventListner('input', handleChange);
  })

  return (
    <form onSubmit={() => console.log(numberOfGuests.current)}>
      <input
        name="numberOfGuests"
        ref={numberOfGuests} />
    </form>
  );
}

At this point, we are writing more code than Controlled Component. But what if we can build a custom hook to handle all of that and re-use the same logic throughout multiple forms within the app.

Hooks

Check out the example below; a custom form validation hook:

import useForm from 'react-hook-form';

function App() {
  const { register, handleSubmit } = useForm();
  const onSubmit = (data) => { console.log(data) };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input name="numberOfGuests"ref={register({ required: true })} />
    </form>
  )
}

As you can see from above, the implementation is clean and simple. There is no render-props wrap around the form, no external components to wrap around individual fields and validation rules are centralized too.

Conclusion

The uncontrolled component can be a better performance neat and clean approach and potentially write a lot less code and better performance. If you find above custom hook example interest and like the syntax. You can find the Github repo and docs link below:

Github: https://github.com/bluebill1049/react-hook-form
Website: https://react-hook-form.com

☕️ Thanks for reading.

Discussion

pic
Editor guide
Collapse
napcoder profile image
Marco Travaglini

Hi Bill, as far as I know you can still attach the "onChange", "onBlur", etc.. to uncontrolled component, no need to use plain events. The key is not to use the "value" prop, so the DOM component can rely on its internal state.

Also, I'm facing with a React bug, where controlled components loose the cursor position: using the uncontrolled component is a good way to avoid this bug, but then I'm not sure how to validate the input: do you have any suggestion?

Collapse
bluebill1049 profile image
Bill Author

Have you tried with this custom hook which I have built? react-hook-form.com/ it may have answer to your question.