React 29 ⚛️ Uncontrolled Inputs with useRef
Controlled inputs are the default in React. The value lives in state, onChange updates it, and the input always reflects what React says it should. This is powerful for validation, formatting, and conditional rendering, but it is not free. Every keystroke triggers a re-render. For a form with many fields, or a field that updates on every character, the re-render cost adds up. Uncontrolled inputs offer an alternative: the value lives in the DOM, and React reads it only when it needs to—typically on submit. The component does not re-render on every keystroke because the state does not change. This is the original HTML behavior, wrapped in a React API.
This chapter covers uncontrolled inputs and the useRef hook that makes them possible. You will learn how to read values from the DOM, how uncontrolled inputs differ from controlled ones, when each approach is appropriate, and the patterns that make uncontrolled forms work with validation, file inputs, and third-party libraries.
Key point: An uncontrolled input manages its own value in the DOM. React does not control it. To read the value, you attach a ref to the input and access ref.current.value when you need it—usually in the submit handler. There is no value prop and no onChange handler. The input behaves like a plain HTML input.
Why uncontrolled inputs exist
The re-render problem. A controlled input re-renders the component on every keystroke. For a single input, this is imperceptible. For a form with fifty fields, or for a component with expensive children, it can cause noticeable lag. An uncontrolled input does not trigger a re-render when the user types. The value is in the DOM, and the component only re-renders when its state changes for other reasons.
The integration problem. Some libraries and browser APIs expect to manage their own DOM elements. A rich text editor, a date picker, a file input, a canvas—these are difficult or impossible to control with React state. They expose their own APIs for reading and writing values. An uncontrolled input with a ref lets React pass the element to the library and read the value when needed.
The simplicity problem. For a form that is submitted once and does not need live validation, controlled inputs add ceremony. Every field needs a state variable and a handler. An uncontrolled form needs a ref for each field and a submit handler. The code is shorter and, for simple cases, clearer.
The file input problem. File inputs cannot be controlled. The browser does not allow JavaScript to set the value of a file input for security reasons. The only way to read a file input is through its files property, which requires a ref or an event handler. This is the original reason React provides useRef.
The performance problem. In large forms, the difference between controlled and uncontrolled can be significant. Uncontrolled forms do not re-render on input, which means the rest of the component tree does not re-render either. For forms with expensive sibling components, this is a real improvement.
a. The useRef hook and basic uncontrolled inputs
useRef returns an object with a current property that persists across renders. When attached to a DOM element with the ref attribute, current points to the DOM node.
import { useRef } from 'react';
function NameForm() {
const inputRef = useRef(null);
const handleSubmit = (e) => {
e.preventDefault();
console.log(inputRef.current.value);
};
return (
<form onSubmit={handleSubmit}>
<input ref={inputRef} defaultValue="" />
<button type="submit">Submit</button>
</form>
);
}
The inputRef is created with useRef(null). The ref={inputRef} attribute attaches it to the input. When the form is submitted, inputRef.current is the DOM input element, and .value is its current value.
The input uses defaultValue instead of value. defaultValue sets the initial value without making the input controlled. The user can change the value freely, and React does not re-render.
There is no onChange handler. The value is not tracked until the form is submitted. The component does not re-render when the user types.
Multiple inputs use multiple refs:
function SignupForm() {
const nameRef = useRef(null);
const emailRef = useRef(null);
const passwordRef = useRef(null);
const handleSubmit = (e) => {
e.preventDefault();
const data = {
name: nameRef.current.value,
email: emailRef.current.value,
password: passwordRef.current.value
};
console.log(data);
};
return (
<form onSubmit={handleSubmit}>
<input ref={nameRef} name="name" />
<input ref={emailRef} name="email" type="email" />
<input ref={passwordRef} name="password" type="password" />
<button type="submit">Sign up</button>
</form>
);
}
Each ref points to a different input. The submit handler reads all three values. The component never re-renders during typing.
b. Reading values and resetting uncontrolled forms
The value is read from ref.current.value. The read happens when the value is needed—usually in the submit handler, but it can happen in any event handler.
const handleCheck = () => {
if (emailRef.current.value.includes('@')) {
setEmailValid(true);
} else {
setEmailValid(false);
}
};
The ref can be read at any time. The input’s value is always current because the DOM is the source of truth.
A checkbox uses ref.current.checked, not .value:
const agreedRef = useRef(null);
const handleSubmit = (e) => {
e.preventDefault();
if (!agreedRef.current.checked) {
alert('You must agree to the terms');
return;
}
};
<input type="checkbox" ref={agreedRef} />
A select uses .value:
const countryRef = useRef(null);
<select ref={countryRef} defaultValue="">
<option value="" disabled>Select</option>
<option value="us">United States</option>
<option value="ca">Canada</option>
</select>
A multi-select uses .selectedOptions:
const tagsRef = useRef(null);
const handleSubmit = (e) => {
e.preventDefault();
const selected = Array.from(tagsRef.current.selectedOptions, (opt) => opt.value);
console.log(selected);
};
<select multiple ref={tagsRef}>
<option value="a">A</option>
<option value="b">B</option>
</select>
A file input uses .files:
const fileRef = useRef(null);
const handleSubmit = (e) => {
e.preventDefault();
const file = fileRef.current.files[0];
console.log(file.name, file.size);
};
<input type="file" ref={fileRef} />
Resetting an uncontrolled form is done by setting the value directly or by resetting the form element:
const formRef = useRef(null);
const handleSubmit = (e) => {
e.preventDefault();
// ... submit
formRef.current.reset();
};
<form ref={formRef} onSubmit={handleSubmit}>
<input name="email" />
<input name="password" type="password" />
<button type="submit">Submit</button>
</form>
The form.reset() method resets all inputs in the form to their default values. This is the simplest way to clear an uncontrolled form.
c. Controlled versus uncontrolled
The two approaches have different trade-offs.
Controlled inputs are the React default. The value is in state, and every keystroke updates it. This makes validation, formatting, and conditional rendering easy. It also means the component re-renders on every keystroke.
Uncontrolled inputs keep the value in the DOM. React does not know the value until it reads it. This means no re-render on keystroke, less code, and easier integration with third-party libraries. It also means live validation is harder—the value is not in state, so there is nothing to validate on change.
The choice depends on the requirements:
| Requirement | Controlled | Uncontrolled |
|---|---|---|
| Live validation | Easy | Hard |
| Conditional formatting | Easy | Hard |
| Re-render cost | Per keystroke | None |
| Integration with DOM libraries | Hard | Easy |
| File inputs | Impossible | Required |
| Simple submit-only forms | Verbose | Concise |
A hybrid approach is common. The form is uncontrolled for most fields, but a few fields that need live validation are controlled. React does not require a choice between the two for the whole form; each input can be either.
The defaultValue prop sets the initial value for an uncontrolled input. It is the uncontrolled equivalent of value. React sets the value once, and the DOM manages it thereafter.
The defaultChecked prop sets the initial checked state for checkboxes and radios.
The defaultValue and defaultChecked props are the only way to set initial values for uncontrolled inputs. Setting value without onChange produces a warning and a read-only input. Setting value with onChange makes the input controlled.
Complete Example Session
// ============================================
// PART 1: BASIC UNCONTROLLED INPUT
// ============================================
import { useRef } from 'react';
function NameForm() {
const inputRef = useRef(null);
const handleSubmit = (e) => {
e.preventDefault();
console.log(inputRef.current.value);
};
return (
<form onSubmit={handleSubmit}>
<input ref={inputRef} />
<button type="submit">Submit</button>
</form>
);
}
// ============================================
// PART 2: MULTIPLE UNCONTROLLED INPUTS
// ============================================
function SignupForm() {
const nameRef = useRef(null);
const emailRef = useRef(null);
const passwordRef = useRef(null);
const handleSubmit = (e) => {
e.preventDefault();
console.log({
name: nameRef.current.value,
email: emailRef.current.value,
password: passwordRef.current.value
});
};
return (
<form onSubmit={handleSubmit}>
<input ref={nameRef} />
<input ref={emailRef} type="email" />
<input ref={passwordRef} type="password" />
<button type="submit">Sign up</button>
</form>
);
}
// ============================================
// PART 3: DEFAULT VALUE
// ============================================
<input ref={inputRef} defaultValue="initial value" />
// Sets the initial value without controlling
// ============================================
// PART 4: UNCONTROLLED CHECKBOX
// ============================================
const agreedRef = useRef(null);
const handleSubmit = (e) => {
e.preventDefault();
console.log(agreedRef.current.checked);
};
<input type="checkbox" ref={agreedRef} defaultChecked={false} />
// ============================================
// PART 5: UNCONTROLLED SELECT
// ============================================
const countryRef = useRef(null);
<select ref={countryRef} defaultValue="">
<option value="" disabled>Select</option>
<option value="us">United States</option>
<option value="ca">Canada</option>
</select>
// ============================================
// PART 6: UNCONTROLLED MULTI-SELECT
// ============================================
const tagsRef = useRef(null);
const handleSubmit = (e) => {
e.preventDefault();
const selected = Array.from(tagsRef.current.selectedOptions, (o) => o.value);
console.log(selected);
};
<select multiple ref={tagsRef}>
<option value="a">A</option>
<option value="b">B</option>
</select>
// ============================================
// PART 7: FILE INPUT
// ============================================
const fileRef = useRef(null);
const handleSubmit = (e) => {
e.preventDefault();
const file = fileRef.current.files[0];
console.log(file.name, file.size);
};
<input type="file" ref={fileRef} />
// ============================================
// PART 8: FORM RESET
// ============================================
const formRef = useRef(null);
const handleSubmit = (e) => {
e.preventDefault();
// submit data
formRef.current.reset();
};
<form ref={formRef} onSubmit={handleSubmit}>
<input name="email" />
<button type="submit">Submit</button>
</form>
// ============================================
// PART 9: HYBRID — CONTROLLED AND UNCONTROLLED
// ============================================
function HybridForm() {
const [email, setEmail] = useState('');
const passwordRef = useRef(null);
return (
<form onSubmit={(e) => {
e.preventDefault();
console.log({ email, password: passwordRef.current.value });
}}>
<input value={email} onChange={(e) => setEmail(e.target.value)} />
<input ref={passwordRef} type="password" />
<button type="submit">Submit</button>
</form>
);
}
// ============================================
// PART 10: READING REF IN OTHER HANDLERS
// ============================================
const emailRef = useRef(null);
const handleCheck = () => {
if (emailRef.current.value.includes('@')) {
setEmailValid(true);
} else {
setEmailValid(false);
}
};
<input ref={emailRef} onBlur={handleCheck} />
The ten parts covered basic uncontrolled input, multiple refs, defaultValue, checkbox, select, multi-select, file input, form reset, hybrid controlled and uncontrolled, and reading the ref in other handlers.
Quick Reference
useRef and Uncontrolled Inputs
| Item | Value |
|---|---|
| Create ref | const ref = useRef(null) |
| Attach to element | <input ref={ref} /> |
| Read value | ref.current.value |
| Read checkbox | ref.current.checked |
| Read file | ref.current.files[0] |
| Initial value | defaultValue |
| Initial checked | defaultChecked |
| Reset form | formRef.current.reset() |
Input Types
| Type | Read From |
|---|---|
| text | ref.current.value |
ref.current.value | |
| password | ref.current.value |
| number | ref.current.value |
| checkbox | ref.current.checked |
| radio | ref.current.checked |
| select | ref.current.value |
| multi-select | ref.current.selectedOptions |
| file | ref.current.files |
Controlled vs Uncontrolled
| Aspect | Controlled | Uncontrolled |
|---|---|---|
| Value lives in | React state | DOM |
| Prop | value | defaultValue |
| Handler | onChange required | None |
| Re-render on input | Yes | No |
| Live validation | Easy | Hard |
| File inputs | Impossible | Required |
| Form reset | Set state | form.reset() |
When to Use Each
| Scenario | Recommendation |
|---|---|
| Simple submit-only form | Uncontrolled |
| Form with live validation | Controlled |
| File upload | Uncontrolled |
| Form with many fields | Uncontrolled |
| Form with conditional fields | Controlled |
| Integration with DOM library | Uncontrolled |
Best Practices
✅ Do This:
// Use useRef for uncontrolled inputs
const inputRef = useRef(null); // ✅
// Use defaultValue for initial value
<input ref={inputRef} defaultValue="initial" /> // ✅
// Read the value on submit
const handleSubmit = (e) => {
e.preventDefault();
console.log(inputRef.current.value);
}; // ✅
// Use form.reset() to clear the form
formRef.current.reset(); // ✅
// Use refs for file inputs
<input type="file" ref={fileRef} /> // ✅
// Mix controlled and uncontrolled as needed
<input value={email} onChange={...} />
<input ref={passwordRef} type="password" /> // ✅
❌ Don’t Do This:
// Don't use value without onChange
<input value={value} ref={inputRef} /> // ❌ warning
// Don't expect the component to re-render on input
<input ref={inputRef} />
// state does not update // ⚠️
// Don't read ref.current.value before mount
const value = inputRef.current.value; // in render // ❌
// Don't use refs for everything
<input ref={ref1} />
<input ref={ref2} />
// ... 20 refs // ⚠️
// Don't forget to read the right property
// checkbox: .checked, not .value // ❌
// Don't try to control a file input
<input type="file" value={file} /> // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Input is read-only | Used value without onChange | Use defaultValue or add onChange |
ref.current is null | Read before mount | Read in event handlers |
| No re-render on input | Uncontrolled by design | Use controlled if re-render needed |
| Checkbox value wrong | Read .value | Read .checked |
| File input not readable | ref not attached | Attach ref, read .files |
| Form does not clear | State not reset | Use form.reset() |
| Too many refs | Uncontrolled everywhere | Use controlled for most, refs for specific |
Real-World Examples
1. Simple Contact Form
const nameRef = useRef(null);
const messageRef = useRef(null);
2. File Upload
const fileRef = useRef(null);
const file = fileRef.current.files[0];
3. Search Input with Ref
const searchRef = useRef(null);
<button onClick={() => search(searchRef.current.value)}>Search</button>
4. Uncontrolled Checkbox
const agreedRef = useRef(null);
if (!agreedRef.current.checked) return;
5. Form Reset
formRef.current.reset();
6. Hybrid Form
<input value={email} onChange={...} />
<input ref={passwordRef} type="password" />
7. Multi-Select
const selected = Array.from(ref.current.selectedOptions, (o) => o.value);
8. Focus Management
const inputRef = useRef(null);
useEffect(() => inputRef.current.focus(), []);
9. Reading Value on Blur
<input ref={emailRef} onBlur={() => validate(emailRef.current.value)} />
10. Third-Party Library Integration
const editorRef = useRef(null);
useEffect(() => {
const editor = new Editor(editorRef.current);
return () => editor.destroy();
}, []);
<div ref={editorRef} />
Visual
Controlled vs Uncontrolled
┌─────────────────────────────────────────────────────────────┐
│ CONTROLLED │
│ │
│ State ──▶ value ──▶ input │
│ ▲ │ │
│ └──── onChange ◀─────┘ │
│ │
│ Every keystroke updates state. │
│ Component re-renders on every keystroke. │
│ │
├─────────────────────────────────────────────────────────────┤
│ │
│ UNCONTROLLED │
│ │
│ input ──▶ DOM value │
│ │ │
│ └── ref ──▶ read on demand │
│ │
│ No state update on keystroke. │
│ Component does not re-render. │
│ Value read when needed. │
│ │
└─────────────────────────────────────────────────────────────┘
useRef Lifecycle
┌─────────────────────────────────────────────────────────────┐
│ useRef(null) │
│ │ │
│ ▼ │
│ { current: null } │
│ │ │
│ ▼ │
│ <input ref={inputRef} /> │
│ │ │
│ ▼ │
│ After mount: { current: <input> } │
│ │ │
│ ▼ │
│ inputRef.current.value → current value │
│ │ │
│ ▼ │
│ After unmount: { current: null } │
│ │
│ ref.current persists across renders. │
│ It does not trigger re-renders when it changes. │
│ │
└─────────────────────────────────────────────────────────────┘
Reading Different Input Types
┌─────────────────────────────────────────────────────────────┐
│ TEXT INPUT │
│ ref.current.value → 'hello' │
│ │
│ CHECKBOX │
│ ref.current.checked → true │
│ ref.current.value → 'on' (default, not useful) │
│ │
│ SELECT │
│ ref.current.value → 'us' │
│ │
│ MULTI-SELECT │
│ ref.current.selectedOptions → HTMLCollection │
│ Array.from(ref.current.selectedOptions, o => o.value) │
│ │
│ FILE │
│ ref.current.files → FileList │
│ ref.current.files[0] → File │
│ │
└─────────────────────────────────────────────────────────────┘
Hybrid Form
┌─────────────────────────────────────────────────────────────┐
│ HYBRID FORM │
│ │
│ <form> │
│ <input value={email} onChange={...} /> │
│ └── CONTROLLED: live validation │
│ │
│ <input ref={passwordRef} type="password" /> │
│ └── UNCONTROLLED: no re-render │
│ │
│ <button onClick={handleSubmit}>Submit</button> │
│ </form> │
│ │
│ Email is validated as the user types. │
│ Password is read only on submit. │
│ Each field uses the approach that fits. │
│ │
└─────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Hook | useRef(null) |
| Attach | ref={inputRef} |
| Read text | inputRef.current.value |
| Read checkbox | inputRef.current.checked |
| Read file | inputRef.current.files[0] |
| Initial value | defaultValue |
| Initial checked | defaultChecked |
| Reset form | formRef.current.reset() |
| Re-render on input | No |
| File input | Uncontrolled only |
| Hybrid | Mix controlled and uncontrolled |
Key takeaways:
- An uncontrolled input manages its own value in the DOM. React does not control it. There is no
valueprop and noonChangehandler. The input behaves like a plain HTML input. useRefprovides access to the DOM node. Therefattribute attaches the ref to the element.ref.currentpoints to the DOM node after mount. The value is read fromref.current.value.defaultValuesets the initial value without making the input controlled.valuewithoutonChangeproduces a read-only input and a warning.defaultValueis the correct prop for uncontrolled inputs.- Different input types read from different properties. Text inputs, selects, and textareas use
.value. Checkboxes and radios use.checked. File inputs use.files. Multi-selects use.selectedOptions. - Uncontrolled inputs do not re-render on keystroke. The component only re-renders when its state changes for other reasons. This is the performance advantage of uncontrolled inputs.
- Live validation is harder with uncontrolled inputs. The value is not in state, so there is nothing to validate on change. The value can be read in
onBluror other event handlers, but the component does not re-render on every keystroke. - File inputs must be uncontrolled. The browser does not allow JavaScript to set the value of a file input. The only way to read it is through a
refor an event handler. - Controlled and uncontrolled can be mixed. Each input in a form can use either approach. A form might use controlled inputs for fields that need live validation and uncontrolled inputs for fields that are simply submitted.
Remember: Controlled inputs are the React default, and for good reason: they make validation, formatting, and conditional rendering straightforward. But they are not the only option. Uncontrolled inputs keep the value in the DOM, avoid re-renders on every keystroke, and integrate naturally with file inputs and third-party libraries. The choice is per input, not per form. Use controlled inputs when you need to react to the value as it changes. Use uncontrolled inputs when you only need the value on submit, when you are working with a file input, or when the re-render cost matters. useRef is the tool that makes uncontrolled inputs possible in React.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!