In React, an uncontrolled component is a form element that keeps its own internal state inside the DOM instead of being fully managed by React state.
Instead of syncing values using onChange and value (controlled pattern), you read the current value only when needed using a reference created with useRef().
This approach is handy for simple forms, file inputs, and performance-sensitive scenarios where you do not need real-time validation on every keystroke.
onChange.ref, e.g. inputRef.current.value..current object that persists across renders and can point to DOM nodes.ref.current.value.The core idea is: let the browser manage the input, and only “peek” into its value when you really need it (for example, on form submission).
Steps to build an uncontrolled component with useRef():
const inputRef = useRef();<input ref={inputRef} />inputRef.current.value
// Basic uncontrolled input using useRef
import React, { useRef } from "react";
function SimpleUncontrolledInput() {
const inputRef = useRef();
const handleShowValue = () => {
alert(`Current value: ${inputRef.current.value}`);
};
return (
<div>
<input type="text" placeholder="Type here..." ref={inputRef} />
<button onClick={handleShowValue}>Show Value</button>
</div>
);
}
export default SimpleUncontrolledInput;
As you type, React does not re-render on each keystroke. When you click Show Value, the current text inside the input is read directly from inputRef.current.value and displayed in an alert.
Here is a conceptual comparison between controlled and uncontrolled components in React:
| Aspect | Controlled Component | Uncontrolled Component |
|---|---|---|
| Data Source | React state | DOM itself |
| Access Value | Via state |
Via ref.current.value |
| Use Case | Form validation, real-time updates | Simple forms, file inputs, performance-critical forms |
| Code Complexity | More verbose | Simpler and direct |
Using useRef(), we can access the input values directly on form submission without storing them in React state.
// Uncontrolled form that reads values only on submit
import React, { useRef } from "react";
function UncontrolledForm() {
const nameRef = useRef();
const emailRef = useRef();
const handleSubmit = (e) => {
e.preventDefault();
const name = nameRef.current.value;
const email = emailRef.current.value;
alert(`Name: ${name}\nEmail: ${email}`);
};
return (
<form onSubmit={handleSubmit}>
<div>
<label>Name:</label>
<input type="text" ref={nameRef} />
</div>
<div>
<label>Email:</label>
<input type="email" ref={emailRef} />
</div>
<button type="submit">Submit</button>
</form>
);
}
export default UncontrolledForm;
The name and email fields are not bound to React state. When you submit the form, their current values are read via nameRef.current.value and emailRef.current.value, and then displayed in an alert.
You can initialize uncontrolled inputs with defaultValue (instead of value). This sets the initial value but does not keep it in sync with React state.
// Uncontrolled input with a defaultValue
import React, { useRef } from "react";
function DefaultValueForm() {
const cityRef = useRef();
const handleSubmit = (e) => {
e.preventDefault();
alert(`City: ${cityRef.current.value}`);
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
defaultValue="Bangalore"
ref={cityRef}
/>
<button type="submit">Submit</button>
</form>
);
}
export default DefaultValueForm;
The input starts with the value Bangalore. The user may change it, and whatever is currently in the box at submit time is read from cityRef.current.value. React does not manage the updates in between.
File inputs are typically handled as uncontrolled components because React cannot directly manage the file objects. Instead, you grab the file from the DOM only when needed.
// Uncontrolled file uploader using a ref
import React, { useRef } from "react";
function FileUploader() {
const fileRef = useRef();
const handleUpload = () => {
const file = fileRef.current.files[0];
if (file) {
alert(`Uploaded File: ${file.name}`);
} else {
alert("No file selected!");
}
};
return (
<div>
<input type="file" ref={fileRef} />
<button onClick={handleUpload}>Upload</button>
</div>
);
}
export default FileUploader;
When you click Upload, the first selected file is read from fileRef.current.files[0]. Only at that moment do you interact with the file object, keeping the rest of your component logic simple and efficient.
You can reset uncontrolled inputs by manually modifying their DOM values via refs instead of resetting React state.
// Manually clearing an uncontrolled input with a ref
import React, { useRef } from "react";
function ResetForm() {
const inputRef = useRef();
const resetField = () => {
inputRef.current.value = "";
};
return (
<div>
<input
type="text"
ref={inputRef}
placeholder="Type something..."
/>
<button onClick={resetField}>Clear</button>
</div>
);
}
export default ResetForm;
With controlled components, clearing a field usually means setting its state to an empty string. Here, you directly modify inputRef.current.value, and the DOM updates instantly without any state change or re-render.
<input type="file"> or password fields.However, uncontrolled components are not ideal when you need:
useRef() primarily for DOM access, not for reactive data that should trigger renders.defaultValue initializes input values without making them controlled.useRef() for all inputs and show the values in an alert on submit.Goal: Understand how to use useRef() to manage DOM elements directly for forms and when uncontrolled components are a better fit than controlled ones.