Uncontrolled Components with useRef: When to Use
Uncontrolled components let the DOM, not React state, manage form input values. Using the useRef hook, you access input values directly from the DOM when needed—typically on form submission. This approach is faster for simple forms (no re-render on keystroke) and is mandatory for file inputs, but offers less real-time validation capability than controlled components. Understanding both patterns helps you choose the right tool for each scenario.
Key Takeaways
- Uncontrolled components use the DOM as source of truth: The input's value lives in the DOM, not in React state
useRefcreates a stable reference:inputRef.current.valueaccesses the input's current DOM value without statedefaultValuesets the initial value: Only changes at mount; subsequent changes don't update the input (unlikevaluein controlled components)- No re-render on keystroke: Uncontrolled components are faster for simple forms because typing doesn't trigger a state update
- File inputs are always uncontrolled: Security restrictions prevent setting
<input type="file">value programmatically—refs are your only option - Controlled is the default best practice: Use uncontrolled only for specific use cases: file uploads, simple forms, non-React library integration
What Is an Uncontrolled Component?
In an uncontrolled component, form data is managed by the DOM itself, not by React state. The input element maintains its own value internally. To read the value, you use a ref to "pull" it from the DOM when you need it.
Compare this to controlled components, where React state drives the input's value at all times (every keystroke updates state, which updates the input).
// UNCONTROLLED: DOM manages the value
function UncontrolledInput() {
const inputRef = useRef(null);
const handleSubmit = (e) => {
e.preventDefault();
console.log('Input value:', inputRef.current.value);
};
return (
<form onSubmit={handleSubmit}>
<input ref={inputRef} type="text" />
<button>Submit</button>
</form>
);
}
// CONTROLLED: React state drives the value
function ControlledInput() {
const [value, setValue] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
console.log('Input value:', value);
};
return (
<form onSubmit={handleSubmit}>
<input value={value} onChange={e => setValue(e.target.value)} />
<button>Submit</button>
</form>
);
}
The uncontrolled version has no state variable. React doesn't "know" what the user typed until the form is submitted. The controlled version re-renders on every keystroke because value changes.
How Do You Access Input Values with useRef?
The useRef hook creates a mutable reference that persists across re-renders. Attach it to the input with the ref attribute, then access inputRef.current.value to read the input's current DOM value.
Simple uncontrolled form:
import React, { useRef } from 'react';
function SimpleUncontrolledForm() {
const nameRef = useRef(null);
const emailRef = useRef(null);
const handleSubmit = (e) => {
e.preventDefault();
const name = nameRef.current.value;
const email = emailRef.current.value;
console.log('Submitting:', { name, email });
// Send data to server, reset form, etc.
};
return (
<form onSubmit={handleSubmit}>
<label>
Name:
<input type="text" ref={nameRef} />
</label>
<label>
Email:
<input type="email" ref={emailRef} />
</label>
<button type="submit">Submit</button>
</form>
);
}
export default SimpleUncontrolledForm;
Key points:
useRef(null)initializes the ref with no value; React setscurrentto the DOM node after rendering.ref={nameRef}attaches the ref to the input; nownameRef.currentis the actual<input>DOM element.nameRef.current.valuereads the input's value from the DOM (returns a string).- No state update needed; values are pulled on submission.
What Is defaultValue and How Does It Differ from value?
Uncontrolled components use defaultValue to set an initial value, not value. This is crucial: defaultValue only applies at mount, and changes to defaultValue afterward have no effect on the input.
import React, { useRef, useState } from 'react';
function UncontrolledWithDefault() {
const inputRef = useRef(null);
const [defaultVal, setDefaultVal] = useState('Hello');
return (
<div>
<input ref={inputRef} type="text" defaultValue={defaultVal} />
<button onClick={() => setDefaultVal('World')}>
Change defaultValue (won't affect input)
</button>
<p>Input value: {inputRef.current?.value}</p>
</div>
);
}
When the button is clicked, defaultVal changes, but the input still shows "Hello". The input's internal DOM value doesn't re-sync because it's uncontrolled.
Controlled component for comparison:
function ControlledWithValue() {
const [value, setValue] = useState('Hello');
return (
<div>
<input value={value} onChange={e => setValue(e.target.value)} />
<button onClick={() => setValue('World')}>
Change value (WILL update input)
</button>
<p>Input value: {value}</p>
</div>
);
}
Here, clicking the button updates the input because the controlled component re-renders with the new value prop. This is the key difference: controlled components stay in sync with React state; uncontrolled components don't.
When Should You Use Uncontrolled Components?
Uncontrolled components have legitimate use cases, but controlled is the default recommendation. Use uncontrolled when:
File inputs (mandatory):
function FileUpload() {
const fileRef = useRef(null);
const handleSubmit = (e) => {
e.preventDefault();
const file = fileRef.current.files[0];
console.log('Selected file:', file.name, file.size);
// Upload to server
};
return (
<form onSubmit={handleSubmit}>
<input type="file" ref={fileRef} />
<button>Upload</button>
</form>
);
}
File inputs cannot be set programmatically for security reasons (users must explicitly choose files). A useRef is your only way to access the selected file.
Simple forms (convenience):
If a form has few fields and you only need values on submission (no real-time validation), an uncontrolled form has less boilerplate:
// Uncontrolled: no state, less code
function LoginForm() {
const userRef = useRef(null);
const passRef = useRef(null);
const handleSubmit = (e) => {
e.preventDefault();
const user = userRef.current.value;
const pass = passRef.current.value;
// Send to API
};
return (
<form onSubmit={handleSubmit}>
<input ref={userRef} placeholder="Username" />
<input ref={passRef} type="password" placeholder="Password" />
<button>Log In</button>
</form>
);
}
Integrating with non-React DOM manipulation:
If you're using a third-party library that directly manipulates the DOM (e.g., jQuery plugins), uncontrolled components play nicely with it:
function JQueryPlugin() {
const rootRef = useRef(null);
useEffect(() => {
// A third-party library manipulates the DOM directly
$(rootRef.current).datepicker(); // jQuery mutates the DOM
}, []);
return (
<div ref={rootRef}>
{/* jQuery plugin renders here; don't fight it with controlled state */}
</div>
);
}
Controlled vs. Uncontrolled: Side-by-Side Comparison
| Aspect | Controlled | Uncontrolled |
|---|---|---|
| Source of truth | React state | DOM element value |
| Data flow | value prop drives input | Input manages itself |
| Access value | Read from state | Pull from ref.current.value |
| Re-renders | Every keystroke (if onChange updates state) | Only on form submission |
| Real-time validation | Easy (validate on each keystroke) | Hard (validate on submission) |
| Initial value | value prop | defaultValue prop |
| Complexity | More code (state + onChange handler) | Less code (just a ref) |
| Use case | Default for most forms | File inputs, simple forms, third-party integration |
Best practice: Start with controlled components. Switch to uncontrolled only if you hit a specific use case (file input, performance issue with a huge form, third-party integration).
Frequently Asked Questions
Why does the warning say "You provided a 'value' prop to a form field without an 'onChange' handler"?
React detects a controlled component (you have value=...) but no onChange handler. The input becomes "read-only" because you're not updating state when the user types. Fix it by adding onChange, or switch to uncontrolled by using defaultValue instead of value.
Can you convert an uncontrolled component to controlled later?
Conceptually yes, but it requires refactoring: add state, use value instead of defaultValue, add onChange to update state. Don't change a live input from defaultValue to value without also adding state and onChange—React will warn about mixing controlled and uncontrolled.
What happens if you put both value and defaultValue on an input?
value wins. React will treat it as controlled, ignore defaultValue, and likely warn you about missing onChange. Always use exactly one: value for controlled, defaultValue for uncontrolled.
Can you reset an uncontrolled form?
Yes, with useRef to access the form element and call its .reset() method:
const formRef = useRef(null);
const handleSubmit = (e) => {
e.preventDefault();
// Process form data
formRef.current.reset(); // Clears all inputs
};
return <form ref={formRef} onSubmit={handleSubmit}> ... </form>;
Is useRef the same as getElementById or querySelector?
Functionally similar, but useRef is React's recommended way because it integrates with React's lifecycle and rendering. Using getElementById in a React component breaks encapsulation and can cause bugs if multiple components try to access the same ID.