Uncontrolled Components
In React, there are two ways to handle form inputs: controlled and uncontrolled. Controlled components keep the value in React state and sync it to the DOM on every render. Uncontrolled components let the DOM itself be the source of truth — you read the value only when you need it, typically on form submission.
For most interactive forms, controlled components are the right choice. But understanding uncontrolled components is valuable because some scenarios genuinely call for them — and because you will encounter them in older codebases and third-party integrations.
The Core Idea: DOM as Source of Truth
In a controlled component, React drives the input:
// Controlled: React state IS the value
const [name, setName] = useState('')
<input value={name} onChange={e => setName(e.target.value)} />In an uncontrolled component, the DOM drives the input and React just reads it when needed:
// Uncontrolled: DOM holds the value, React reads it on demand
const nameRef = useRef(null)
<input ref={nameRef} defaultValue="" />Reading Values with useRef
The standard way to access an uncontrolled input's value is with the useRef hook. You attach the ref to the DOM element, and React sets ref.current to that element after the component mounts.
import { useRef } from 'react'
function SimpleForm() {
const usernameRef = useRef(null)
const passwordRef = useRef(null)
function handleSubmit(e) {
e.preventDefault()
const username = usernameRef.current.value
const password = passwordRef.current.value
console.log({ username, password })
// submit to your API...
}
return (
<form onSubmit={handleSubmit}>
<label>
Username
<input ref={usernameRef} type="text" defaultValue="" />
</label>
<label>
Password
<input ref={passwordRef} type="password" />
</label>
<button type="submit">Log in</button>
</form>
)
}Notice there is no onChange handler and no state. The component never re-renders as the user types — you only read ref.current.value on submit.
defaultValue vs value
React provides defaultValue (and defaultChecked for checkboxes) to set the initial value of an uncontrolled element without taking control of it afterwards.
// Sets "React" as the initial text but allows the user to change it freely.
// React does NOT update this when defaultValue changes after the first render.
<input type="text" defaultValue="React" ref={inputRef} />
// A textarea works the same way:
<textarea defaultValue="Write something here..." ref={textareaRef} />
// A select box:
<select defaultValue="medium" ref={selectRef}>
<option value="small">Small</option>
<option value="medium">Medium</option>
<option value="large">Large</option>
</select>File Inputs Are Always Uncontrolled
The <input type="file"> element is a special case — it is always uncontrolled in React because its value can only be set by the user (browsers prevent programmatic file path setting for security reasons). You must always use a ref to access the selected files.
import { useRef } from 'react'
function FileUploader() {
const fileInputRef = useRef(null)
function handleUpload(e) {
e.preventDefault()
const files = fileInputRef.current.files
if (!files || files.length === 0) {
alert('Please select a file')
return
}
const file = files[0]
console.log('File name:', file.name)
console.log('File size:', file.size, 'bytes')
console.log('File type:', file.type)
// You would now create a FormData or use the File object directly
const formData = new FormData()
formData.append('file', file)
// fetch('/api/upload', { method: 'POST', body: formData })
}
return (
<form onSubmit={handleUpload}>
<input type="file" ref={fileInputRef} accept="image/*" />
<button type="submit">Upload</button>
</form>
)
}A Complete Uncontrolled Registration Form
Here is a more realistic form that collects several fields and reads all values at submit time:
import { useRef } from 'react'
function RegistrationForm() {
const firstNameRef = useRef(null)
const lastNameRef = useRef(null)
const emailRef = useRef(null)
const avatarRef = useRef(null)
function handleSubmit(e) {
e.preventDefault()
const payload = {
firstName : firstNameRef.current.value.trim(),
lastName : lastNameRef.current.value.trim(),
email : emailRef.current.value.trim(),
avatar : avatarRef.current.files[0] ?? null,
}
// Basic validation at submit time
if (!payload.firstName || !payload.email) {
alert('First name and email are required.')
return
}
console.log('Submitting:', payload)
}
function handleReset() {
// Manually reset each input because React won't manage the values
firstNameRef.current.value = ''
lastNameRef.current.value = ''
emailRef.current.value = ''
// File inputs are reset by replacing them or using the form's reset()
}
return (
<form onSubmit={handleSubmit} onReset={handleReset}>
<div>
<label>First name</label>
<input ref={firstNameRef} type="text" defaultValue="" />
</div>
<div>
<label>Last name</label>
<input ref={lastNameRef} type="text" defaultValue="" />
</div>
<div>
<label>Email</label>
<input ref={emailRef} type="email" defaultValue="" />
</div>
<div>
<label>Avatar (optional)</label>
<input ref={avatarRef} type="file" accept="image/*" />
</div>
<button type="submit">Register</button>
<button type="reset">Clear</button>
</form>
)
}Integrating with Non-React Libraries
One of the strongest use cases for uncontrolled components is integrating a third-party library that manages its own DOM — for example a rich text editor, a date picker, or a drag-and-drop library. You attach a ref, let the library take over the DOM node, and read the value from the library's API on submit.
import { useRef, useEffect } from 'react'
// Imagine "Quill" is a rich-text editor library
import Quill from 'quill'
function RichTextEditor({ onSave }) {
const editorRef = useRef(null)
const quillRef = useRef(null) // holds the Quill instance
useEffect(() => {
// Give Quill control of the DOM node — React stays hands-off
quillRef.current = new Quill(editorRef.current, { theme: 'snow' })
return () => {
// Cleanup: destroy the Quill instance on unmount
quillRef.current = null
}
}, [])
function handleSave() {
// Read content from Quill's API, not from React state
const html = editorRef.current.querySelector('.ql-editor').innerHTML
onSave(html)
}
return (
<div>
<div ref={editorRef} />
<button onClick={handleSave}>Save</button>
</div>
)
}Controlled vs Uncontrolled: When to Use Which
Scenario | Use |
|---|---|
Live validation as the user types | Controlled |
Conditionally enable a submit button | Controlled |
Instant field-level feedback (character count, password strength) | Controlled |
Dependent fields (e.g. country changes available states) | Controlled |
Simple one-time-read form (search box, login) | Either works |
File input | Uncontrolled (always) |
Integrating a third-party DOM library | Uncontrolled |
Performance-critical form with many fields and no live feedback | Uncontrolled |