The controlled and the uncontrolled way to clear React Input component value after submit

Written by David Abram

While importing the Input component from our internal component library for a company-related project, we encountered an issue that compelled us to completely reinvent how this component works. We realized that there was no way for a parent component to affect the value of the rendered <input> HTML element, and after reading through the source code, we decided to look for solutions and ultimately refactor our Input component.

Contents



Background

Let’s provide some context; one of our developers was working on a project where he wanted to create a form for a web application using components from our CroCoder component library. He noticed that he couldn't change or clear the value of the HTML <input> element via the parent component. As a result, after submitting the form, the website visitor would still see the values they have entered displayed in the HTML <input> element, even though the Input component would interpret the HTML <input> element as empty.

Underneath is a demo of how our component worked before the implemented fix. We added a small console next to the form, so that you can see the state inside of the component. Type something in the form and submit it. You should see the console correctly display whatever you wrote in the input. However, you will also see that the HTML <input> element's value will not clear, and if you try resubmitting it, the console will show that the state of the input fields considers them empty, even when they aren't.

Contact CroCoder

Try submitting the form!

This makes the UI seem lackluster because nothing gives any indication to the website visitor that the form was submitted after something was typed in. Our developer wanted to improve the UI by clearing the value from the HTML <input> element after the form was submitted. Finally, after reading through the source code, he found out that the Input component from our component library didn't accept a ref as its prop, even though it was an uncontrolled component by design.

We would like to show you three possible approaches that make this form work in the React world. We would take into account several different viewpoints including the one of a component library consumer; i.e., the developer using the component library as a package.

Input as a controlled component

The most obvious solution to this problem is to transform the Input component into a controlled one by adding a state to the parent component itself. This way the rendered HTML <input> element would be driven by the parent component’s state, which would allow us to alter the input value, delete it, pass it to other UI elements, etc. In this solution the onChange event handler would return the input value to the parent component, and the parent component would be able to track the changes within the HTML <input> element saving the input value to its state.

/*
  Input component that is exported from component library
  
  Controlled component's input element should recieve 
  value and onChange props for Input component to function properly
  
  The inputValue should be stored in the parent's component state 
*/
const InputComponent = ({
  label,
  inputValue,
  onInputChange,
}) => {
  return (
    <div>
      <span>{label}</span>
      <input value={inputValue} onChange={onInputChange} />
    </div>
  );
}  
/*
  The parent component code that will be implemented 
  in the consumer's web app

  Input value should be stored in component's state,
  onChange handler should set the Input value  
*/
const ParentFormComponent = () => {
  
  const [emailInputValue, setEmailInputValue] = useState('');

  const handleEmailChange = useCallback((e) => {
    setEmailInputValue(e.target.value);
  }, [setEmailInputValue]);

  const [nameInputValue, setNameInputValue] = useState('');

  const handleNameChange = useCallback((e) => {
    setNameInputValue(e.target.value);
  }, [setNameInputValue]);

  /*
    Getting the values from each input is straightforward;
    just get it from the component's state
  */
  const handleSubmit = useCallback(() => {
    console.log(
      `{
       email: ${emailInputValue || null},
       name: ${nameInputValue || null} 
      }`
    );
    setEmailInputValue('');
    setNameInputValue('');
  }, [emailInputValue, nameInputValue])

  return (
    <>
      <InputComponent 
        label="E-mail"
        inputValue={emailInputValue}
        onInputChange={handleEmailChange} 
      />
      <InputComponent
        label="Name"
        inputValue={nameInputValue}
        onInputChange={handleNameChange}
      />
      <button onClick={handleSubmit}>Submit</button>
    </>
  );
}

React docs recommend the controlled component approach to implement forms. The biggest upside of the controlled Input component is that form data isn't handled by the DOM, but as you can see in the previous code, the form data is handled by the parent form component.

This approach forces the component library consumer to implement state management and change handlers in the parent form component for each Input component instance. Even though this is the usual way of handling forms, it can get annoying implementing so much boilerplate code. In addition, there is no business logic present in the Input component.

Input as an uncontrolled component

But what if you want to keep your Input component uncontrolled? Another solution we want to show you is adding the ability to pass a ref via props to the rendered HTML <input> element. The parent component passes the ref to the Input component to access and change the HTML <input> element’s value via ref.current.

/*
  Input component that is exported from component library
  
  Uncontrolled component's input element should recieve 
  ref prop for Input component to function properly
*/
const InputComponent = ({
  label,
  inputRef
}) => {
  return (
    <div>
      <span>{label}</span>
      <input ref={inputRef} />
    </div>
  );
}
/*
  The parent component code that will be implemented
  in the consumer's web app
*/
const ParentFormComponent = () => {

  const emailInputRef = useRef(null);

  const nameInputRef = useRef(null);

  /*
    You can get and set the values of each input element 
    by getting or setting refInstance.current.value
  */
  const handleSubmit = useCallback(() => {
      console.log(
        `{
         email: ${emailInputRef.current.value || null},
         name: ${nameInputRef.current.value || null}
        }`
      );
      emailInputRef.current.value = '';
      nameInputRef.current.value = '';
  }, [])

  return (
    <>
      <InputComponent label="E-mail" inputRef={emailInputRef} />
      <InputComponent label="Name" inputRef={nameInputRef} />
      <button onClick={handleSubmit}>Submit</button>
    </>
  );
}

However, this solution would give the parent component the ability to easily change some HTML <input> element attributes like style, class and even the type of the input, which could bring about some unexpected consequences.

For example, try submitting this form:

E-mail

Name

Try submitting the form!

With a few extra lines of code, one can easily fiddle with your component. Even though you can always access any HTML element via document.querySelector and change all of the attributes, handlers, etc., we didn’t really want to create an easy path for doing this via our Input component API. We wanted a clear-cut solution, a component API that has a precise scope with limited peddling options.

Solution

As stated previously, our Input component was always imagined as an uncontrolled component. Having an internal state would require adding additional logic; an event handler for each and every way your data can change, while piping all of the input state to the parent component. We wanted to have a component that would have its own ‘custom’ and streamlined API, instead of the HTML <input> element’s API.

Fortunately, there is a hook called useImperativeHandle that works wonders with another piece of React API called forwardRef.

By using useImperativeHandle we can create a ‘custom component API’ which can be used in the parent component by accessing ref.current thanks to forwardRef.

Using the combination of both, we control what's exposed as the reference for our Input component. In our case we decided to expose a function setInputValue to set the value of the HTML input component, a function clearInputValue to clear it and a functiongetInputValue to retreive it.

Check out the code bellow:

/*
  Input component that is exported from component library
  
  ref is passed by forwardRef to the componentx
*/
const InputComponent = forwardRef((
  {
    label,
  },
  ref
) => {
  const inputRef = useRef(null);

  const getValue = useCallback(
    () => inputRef.current.value,
    [],
  );

  const setValue = useCallback((value) => {
    inputRef.current.value = value;
  }, []);

  const clearValue = useCallback(() => {
    inputRef.current.value = "";
  }, []);

  /*
    useImperativeHandle customizes the instance value
    that is exposed to parent components when using ref

    Parent component can call any function or get any
    properties returned by the handle passed as second
    argument
  */
  useImperativeHandle(
    ref,
    () => ({ getValue, setValue, clearValue }),
    [],
  );

  return (
    <div>
      <span>{label}</span>
      <input ref={inputRef} />
    </div>
  );
});
  /*
    The parent component code that will be implemented
    in the consumer's web app
  */
const ParentFormComponent = () => {

  const emailInputRef = useRef(null);

  const nameInputRef = useRef(null);


    /*
      You can get and the values of each input element 
      by calling refInstance.current.getValue() and set / clear
      the values of each input element with refInstance.current.setValue() 
      or refInstance.current.clearValue()
    */
  const handleSubmit = useCallback(() => {
    console.log(
      `{
       email: ${emailInputRef.current.getValue() || null},
       name: ${nameInputRef.current.getValue() || null} 
      }`
    );
    emailInputRef.current.clearValue();
    nameInputRef.current.clearValue();
  }, [])

  return (
    <>
      <InputComponent label="E-mail" ref={emailInputRef} />
      <InputComponent label="Name" ref={nameInputRef} />
      <button onClick={handleSubmit}>Submit</button>
    </>
  );
};

With this small example of parent component usage, you can see how much more elegant the consumer code can get.

Conclusion

Having a clear component API and ease of use was an important criteria for us. To fix our form we first had to rewrite our Input component to use forwardRef / useImperativeHandle.

When implementing forms in React, using controlled components is always the preferred approach, but when implementing component libraries, you need to be aware that this solution leaves the consumer implementing all the business logic, i.e., every handler and the whole state management.

The uncontrolled component approach without forwardRef / useImperativeHandle can lead to your DOM getting changed by accident and/or outside purview of React.

Although we have reinvented the Input component as an uncontrolled component using forwardRef and useImperativeHandle, I wouldn't suggest taking this approach unless you are implementing that component as part of a component library. Missuse of refs could lead you to pass refs through multiple components and a confusing data flow.

We are actively working on expanding our internal component library, always striving to create neat and tidy component APIs. That is why long term thinking and reusability are key factors in our decision making when it comes to logic building of our components. We hope you found our solution helpful, and of course, feel free to comment or contact us about any ambiguities you might encounter while going through this post.

David Abram
Spends his time untangling software architectures and doing DevOps. Likes to build stuff. Connect with him on Twitter and LinkedIn