ReactUncontrolled Components

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:

JSX
// 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:

JSX
// Uncontrolled: DOM holds the value, React reads it on demand
const nameRef = useRef(null)
<input ref={nameRef} defaultValue="" />
Note
The key distinction is `value` vs `defaultValue`. A controlled input has `value` (React-managed). An uncontrolled input uses `defaultValue` to set the initial value, then the DOM takes over.
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.

JSX
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.

JSX
// 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>
Warning
Never mix `value` and `defaultValue` on the same element. If you provide `value`, React controls the input. If you want an uncontrolled input, use `defaultValue`. Providing `value` without an `onChange` handler creates a read-only input and produces a React warning.
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.

JSX
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:

JSX
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>
  )
}
Tip
Notice the `handleReset` function. Because React does not manage the values, you need to reset them imperatively. Alternatively, call `formRef.current.reset()` on the `<form>` element itself to trigger native browser reset behaviour.
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.

JSX
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

Note
The React documentation recommends controlled components for most cases because they make the data flow explicit and predictable. Uncontrolled components are a pragmatic escape hatch, not the default.