Search code examples
reactjsreact-hooks

useState or useRef for form submit


Normally when I handle form submits, I use useState for input values and set onChange event as

const [inputValues, setInputValues] = useState({
    title: "",
    address: "init"
  });

  function submitHandler(e) {
    e.preventDefault();

    const meetupData = {
      meetupTitle: inputValues.title,
    };

    console.log(meetupData);
  }
  return (
    <form onSubmit={submitHandler}>
      <div>
        <label htmlFor="title">Meetup Title</label>
        <input
          type="text"
          required
          id="title"
          value={inputValues.title}
          onChange={(e) =>
            setInputValues({ ...inputValues, title: e.target.value })
          }
        />
      </div>
      <div>
        <button>Add Meetup</button>
      </div>
    </form>
  );

but in some tutorial I found out it's written using useRef as

const titleInputRef = useRef();

  function submitHandler(e) {
    e.preventDefault();
    const enteredTitle = titleInputRef.current.value;

    const meetupData = {
      title: enteredTitle,
    };

    console.log(meetupData);
  }
  return (
    <form onSubmit={submitHandler}>
      <div>
        <label htmlFor="title">Meetup Title</label>
        <input type="text" required id="title" ref={titleInputRef} />
      </div>
      <div>
        <button>Add Meetup</button>
      </div>
    </form>
  );

and I am trying to understand what's the difference and which one could be better in general performance ?


Solution

  • Your first example is known as a controlled component1, while the second example is known as uncontrolled component.

    Controlled Components

    An input form element whose value is controlled by React state is called a controlled component.

    From old react docs:

    With a controlled component, the input’s value is always driven by the React state. While this means you have to type a bit more code, you can now pass the value to other UI elements too, or reset it from other event handlers.

    So since the values of controlled components are driven by react state, this means you can easily access them and react to their changes. For example you can disable some form element based on value of another form element, show an error message if value of some form element is not equal to something, etc. Below is one such simplified example:

    export default function ExampleComponent() {
      const [fname, setFName] = React.useState('');
      const [lname, setLName] = React.useState('');
      return (
        <form>
          <input
            type="text"
            placeholder="First name"
            value={fname}
            onChange={(e) => setFName(e.target.value)}
          />
          <input
            type="text"
            placeholder="Last name"
            value={lname}
            onChange={(e) => setLName(e.target.value)}
          />
          {fname && fname.includes('@') && <div>No @ allowed in first name</div>}
        </form>
      );
    }
    

    However since the values are stored in state this also means changing value of form element will cause re-render of the component where such state lives (and of its children unless they are memoized); in the above example such component is the ExampleComponent. In most of the cases this should not be an issue. If it becomes an issue, then you can look into optimization; for example see if you can use React.memo with children of the component where the form state lives (in our example this would not be possible directly because children of ExampleComponent are built in browser form components and not custom react components).

    Uncontrolled Components

    Uncontrolled components manage their own state internally rather than relying on React state. You can use a ref to get values of an uncontrolled form element.

    Here are some scenarios from (old) react docs when you might want to use them:

    It can sometimes be tedious to use controlled components, because you need to write an event handler for every way your data can change and pipe all of the input state through a React component. This can become particularly annoying when you are converting a preexisting codebase to React, or integrating a React application with a non-React library. In these situations, you might want to check out uncontrolled components, an alternative technique for implementing input forms.

    One time retrieval of values of form elements during submit

    If you need one time retrieval of values of form elements during submit in such case you can also create a form data object in submit handler and read values directly from that (without using controlled or uncontrolled components):

    export default function MyForm() {
      function handleSubmit(e) {
        // Prevent the browser from reloading the page
        e.preventDefault();
    
        // Read the form data
        const form = e.target;
        const formData = new FormData(form);
    
        // Access fields one at a time
        const myInputValue = formData.get("myInput")
    
        // You can pass formData as a fetch body directly:
        fetch('/some-api', { method: form.method, body: formData });
    
        // Or you can work with it as a plain object:
        const formJson = Object.fromEntries(formData.entries());
        console.log(formJson);
      }
    
      return (
        <form method="post" onSubmit={handleSubmit}>
          <label>
            Text input: <input name="myInput" defaultValue="Some initial value" />
          </label>     
          <button type="submit">Submit form</button>
        </form>
      );
    }
    

    1 The terms “controlled” and “uncontrolled” usually refer to form inputs, but they can also describe where any component’s data lives. Data passed in as props can be thought of as controlled (because the parent component controls that data). Data that exists only in internal state can be thought of as uncontrolled (because the parent can’t directly change it). Source.