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.
Show me the good stuff
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:
React.createRef
(from React 16.3)- 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:

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.

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:
– reactjs.org
useImperativeHandle
customizes the instance value that is exposed to parent components when usingref
. As always, imperative code using refs should be avoided in most cases.useImperativeHandle
should be used withforwardRef
:
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 alongprops
. The secondref
argument only exists when you define a component withReact.forwardRef
call. Regular function or class components don’t receive theref
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 touseImperativeHandle
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.

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