React Forms by Aditya Tyagi

A Guide To Working With Forms And Input Fields in React

All about controlled and uncontrolled components in React

The best way to understand the basics of any framework is to develop a CRUD application, and every CRUD application will always have a form with different input fields. While working with forms in React, you’ll come across fancy terms like “Controlled” and “Uncontrolled” components. My aim with this write-up is to break down these fancy terms in plain English.

The prerequisite for the blog post is that you have a basic understanding of HTML forms. If not, you can refer to this for a quick refresher.

Refs and the DOM

ref is an attribute that is present on all the input elements by default. They allow us to access other DOM elements and work with them.

Refs provide a way to access DOM nodes or React elements created in the render method.

reactjs.org

To understand what ref can do, let’s see how forms are used. If you have any experience with React, you’ll know what useState is and how it can work with form fields. For others, let’s consider this small example:

// App.js
import React, { useState } from "react";
import { render } from "react-dom";

// Main App component
const App = (props) => {
  const [name, setName] = useState("");

  // updates name on every keystroke/onChange event
  const nameChangeHandler = (e) => {
    setName(e.target.value);
  };

  // form submit handler
  const onSubmit = (e) => {
    e.preventDefault();

    // form
    console.log({
      userName: name
    });

    // reset the form after using the value
    setName("");
  };

  return (
    <div>
      <h2>Forms and Input Fields in React by Aditya Tyagi</h2>
      <form onSubmit={onSubmit}>
        <input
          id="name"
          value={name}
          onChange={nameChangeHandler}
          type="text"
        />
        <button type="submit">Submit</button>
      </form>
    </div>
  );
};

render(<App />, document.getElementById("root"));

Here, we update the value of the user’s name with every keystroke, using the onChange event handler present on input and store it in the name variable. We feed that state back into the input using the value attribute. We then use the form’s submit handler to use the variable’s value and at last clearing the input. This is a perfect example of a “controlled” input field/component.

This is a perfect way to manage the state of the form. But updating the state i.e. name with every keystroke is inefficient when we only need the state during form submit.

This is the place where refs come into play.

How do refs work?

Refs setup a connection between the HTML element that is rendered in the DOM and the JavaScript code. We do this by using a react hook called – useRef()

A side-note:

  1. React.createRef (from React 16.3)
  2. The useRef Hook (from React 16.8)

Like all hooks, useRef() follows all the rules of React hooks. It takes a default value with which we can initialize it, but that’s optional. It returns a value which will allow us to work with that ref later once we connect it to an HTML DOM element. For connecting, we’ll use the help of a built-in attribute/prop called ref which is present on every HTML DOM element.

// App.js with Ref

import React, { useRef } from "react";
import { render } from "react-dom";

// Main App component
const App = (props) => {
  // the ref that will connect to input field
  const nameRef = useRef();

  // form submit handler
  const onSubmit = (e) => {
    e.preventDefault();

    console.log({
      // every ref will ALWAYS have the "current" property, which is the HTML DOM element
      userName: nameRef.current.value
    });

    // reset the form after using the value - NOT RECOMMENDED
    nameRef.current.value = "";
  };

  return (
    <div>
      <h2>Forms and Input Fields in React by Aditya Tyagi</h2>
      <form onSubmit={onSubmit}>
        <input id="name" ref={nameRef} type="text" />
        <button type="submit">Submit</button>
      </form>
    </div>
  );
};

render(<App />, document.getElementById("root"));

Because you can connect the ref to any HTML DOM element, it doesn’t mean you should. You’ll use ref primarily with forms.

At the end of it the nameRef will actually hold a complete DOM element, on it’s current property. If I console the nameRef.current, we can see we have the entire input native HTML DOM element:

// CASE 1: will return an object with the native element on the "current" property
console.log(nameRef)           // {current: HTMLInputElement}

// CASE 2: will return the native element
console.log(nameRef.current)   // <input id="name" type="text"></input>

For a better understanding, this is what it will render in case 2:

The current property holds the value of the DOM element connected
The current property holds the value of the DOM element connected

NOTE: They highly recommended to NOT manipulate the DOM imperatively, i.e. Not using React. When we are using useState to manipulate the DOM, its fine and is the ideal way to do things. Use cases of useRef are different and should not be your go-to solution to manipulate DOM like re-setting from fields, updating form field values, etc. Doing read-only actions is still acceptable with ref because you are not changing any state.

// reset the form after using the value - NOT RECOMMENDED AS WE ARE CHANGING STATE
nameRef.current.value = "";

When to use ref?

The ideal place to use ref is when you want to do read-only actions. You will sometimes have use cases where you want to read a value and not change it. If this is the case, you can avoid using useState. It will cause a lot of superfluous code.

According to official docs, a few good use cases for refs:

  • Managing focus, text selection, or media playback.
  • Triggering imperative animations – where imperative animations means defining animations using JavaScript
  • Integrating with third-party DOM libraries.

Avoid using refs for anything that can be done declaratively.

A use case of ref

Consider a use case wherein you want to bring focus to the input DOM element when there is an error during form submission. We can do this using ref

In the example below, if the user tries to submit a form with no input, it will be an invalid submit action and the form will have focus.

Name input getting focus on invalid form submission
Name input getting a focus on invalid form submission
import React, { useRef } from "react";
import { render } from "react-dom";

// Main App component
const App = (props) => {
  // the ref that will connect to input field
  const nameRef = useRef("sadasdadad");

  // form submit handler
  const onSubmit = (e) => {
    e.preventDefault();


    // if the input is invalid
    if(!nameRef.current.value) {
      nameRef.current.focus();
    }

    // submit the form with valid values
    console.log({
      // every ref will ALWAYS have the "current" property, which is the HTML DOM element
      userName: nameRef.current.value
    });

    // reset the form after using the value
    nameRef.current.value = "";
  };

  return (
    <div>
      <h2>Forms and Input Fields in React by Aditya Tyagi</h2>
      <form onSubmit={onSubmit}>
        <input id="name" ref={nameRef} type="text" />
        <button type="submit">Submit</button>
      </form>
    </div>
  );
};

render(<App />, document.getElementById("root"));

Using ref with functional component

Yes, every HTML DOM element has a ref prop on it using which you can target it. But you cannot use ref on React functional components. For example:

// Functional Component for React
const Age = (props) => {
  return (
    // React Fragment
    <>
      <label>Age </label>
      <input {...props} />
    </>
  );
};


// in App.js
const ageRef = useRef();
<Age ref={ageRef}/> // INVALID CODE

But the maintainers of React are really considerate. You can use ref with functional components using another hook. This hook helps to work with functional components imperatively, i.e. not by passing some state to it via props, but calling a function which changes something inside the component, from the parent. This is much like a parent-child relationship management in React.

This is something you should not do very often. Putting out here because my job is to present all the weapons in front of my genuine readers, because I don’t want them to go to war (a.k.a work) ill-equipped.

Consider a use case wherein you want to bring focus to the input element programmatically from the parent component where the child is being rendered. I’ll be adding 2 custom functions focusAgeInputField and isAgeValid to the Age functional component.

  // focus the age input - will be called from outside i.e App.js
  const ageInputRef = useRef();

  const focusAgeInputField = () => {
    ageInputRef.current.focus();
  }

  const isAgeValid = () => {
    return !!ageInputRef.current.value;
  }

So, what we are trying to do is access the component or the functionality inside the component imperatively and this is where we use useImperativeHandle hook. You can learn more about it through this detailed article by Anik.

According to the official docs:

useImperativeHandle customizes the instance value that is exposed to parent components when using ref. As always, imperative code using refs should be avoided in most cases. useImperativeHandle should be used with forwardRef:

reactjs.org

When using useImperativeHandle, you are fully aware that you want to control the input NOT through the state-prop management, not by controlling the state of the component from the parent component, but directly calling or manipulating something in the component programmatically. This is neither advised, nor recommended, but you should know it anyway!

Working with useImperativeHandle

The useImperativeHandle hook takes in two arguments:

  • ref: Passing the reference point coming from outside. This ref is the second argument that comes along props. The second ref argument only exists when you define a component with React.forwardRef call. Regular function or class components don’t receive the ref argument, and ref is not available in props either.

Ref forwarding is not limited to DOM components. You can forward refs to class component instances, too.

// ref is the connection point that will connect the internal ref to the "ref" attribute on the <Age ref={ } />
const Age = (props, ref) => {
  const ageInputRef = useRef();
}
  • A callback function: This function must return an object. This object will contain all the data you will be able to use from outside. The name of the properties of the object will be the ones you’ll use from outside. The second argument on the functional component i.e. ref will then be passed to useImperativeHandle as the first argument.
  useImperativeHandle(ref, () => {
    return {
      focus: focusAgeInputField,
      isValid: isAgeValid
    }
  })

In order to export our ref argument in the Age functional component, we need to export the functional component in a special manner.

Using forwardRef

We need to wrap the functional component using React.forwardRef so that the ref argument can be activated.

From the official react docs:

Ref forwarding is a technique for automatically passing a ref through a component to one of its children. This is typically not necessary for most components in the application. However, it can be useful for some kinds of components, especially in reusable component libraries.

reactjs.org

React.forwardRef will take the functional component as the first argument and return a custom functional component which can be bound to a ref

// Final Age functional component
const Age = React.forwardRef((props, ref) => {
  const ageInputRef = useRef();

  // focus the age input - will be called from outside i.e App.js
  const focusAgeInputField = () => {
    ageInputRef.current.focus();
  }

  const isAgeValid = () => {
    return !!ageInputRef.current.value;
  }

  useImperativeHandle(ref, () => {
    return {
      focus: focusAgeInputField,
      isValid: isAgeValid
    }
  })

  return (
    // React Fragment
    <>
      <label>Age </label>
      <input {...props} />
    </>
  );
});

Using the Age functional component to access the two methods on it via ref. There is one more thing that the eagle-eye readers might have noticed i.e. the <> and </>. These are called React Fragments.

// in App.js

// adding a ref to the Age component - NOW VALID
const ageRef = useRef();
<Age ref={ageRef}/>

// updating the onSubmit function
// if age is invalid
if(!ageRef.current.isValid()) {
  ageRef.current.focus();
}

Putting it all together, we can now submit the function with an empty Age input and as a result, it will focus on that field.

Age input getting focus on invalid submission
Age input getting focus on invalid submission

Uncontrolled v/s Controlled Components

All of the above things that you just learnt was nothing but working with “uncontrolled” components. By uncontrolled, it means that you are imperatively handling the value/state and are not using React to do that.

With “controlled” components and input fields, you use state to read, write and update the component states. This is when you use useState or useReducer to create a state and then bind that state to your component/input field. Once you do that, it becomes a “controlled” input field/component.

As per the official docs:

Since an uncontrolled component keeps the source of truth in the DOM, it is sometimes easier to integrate React and non-React code when using uncontrolled components. It can also be slightly less code if you want to be quick and dirty. Otherwise, you should usually use controlled components.

– reactjs.org

While researching for this blog post, I also landed on this piece of gem by Gosha Arinich.

defaultValue v/s value

You cannot use value attribute on uncontrolled components/form fields. If you wish to give a default value to an uncontrolled form field, use defaultValue. Read more about it here.

<input defaultValue="Default name" id="name" ref={nameRef} type="text" />

Bonus: A playground

And there you have it. Try to play around and get your hands dirty! I hope you have found this useful. Thank you for reading.

1 comment / Add your comment below

Leave a Reply

Your email address will not be published. Required fields are marked *