跪拜 Guibai
← Back to the summary

React Form Data Flow: When State Should Own the Input and When the DOM Should

React Controlled vs Uncontrolled Components: Explaining Form Data Flow from a Single Input Box

When working with forms, we often encounter two seemingly similar approaches: one binds value and onChange to the input box, while the other only attaches a ref and reads the content upon submission.

Both approaches can capture user input, but the underlying data flow is completely different. The real question to ask is not "which approach is more advanced," but rather: Is the current value of the input box managed by React state, or by the DOM itself?

This article uses a React 19 form example, starting from a single input box, to progressively explain controlled components, uncontrolled components, multi-field forms, and real-time validation.

1. First, Grasp the Core: Who is the Single Source of Truth

In form scenarios, the two types of components can be distinguished in one sentence:

Their input processes can be simplified as:

Controlled component: User input -> onChange -> Update state -> React re-renders -> value updates

Uncontrolled component: User input -> DOM updates value itself -> Click submit -> Read via ref

This distinction further affects real-time validation, button states, field linkage, reset methods, and component rendering behavior.

2. Controlled Components: Input Values Always Pass Through React

Let's look at the controlled input box in the example:

import { useState } from 'react'

function ControlledInput() {
  const [value, setValue] = useState('')

  return (
    <>
      <label htmlFor="nickname">Nickname</label>
      <input
        id="nickname"
        value={value}
        onChange={(event) => setValue(event.target.value)}
      />
      <p>Current input: {value || 'Empty string'}</p>
    </>
  )
}

There are two key attributes here:

Suppose the user types the letter A. The process is as follows:

  1. The browser triggers onChange.
  2. A is obtained via event.target.value.
  3. setValue('A') updates the state.
  4. The component re-renders.
  5. The new value="A" is passed back to the input box.

Therefore, React state is the single source of truth. We can display this value in the input box and also use it to synchronously update preview areas, remaining character counts, or submit buttons.

Why Setting Only value Doesn't Work

If you set value on an input box but don't update it in onChange:

<input value={value} />

Although the user types content on the keyboard, on the next render, React will still pass the old value to the input box. The result is that the input box appears uneditable.

Therefore, controlled input boxes typically come in pairs: value + onChange, while checkboxes typically use checked + onChange.

3. Uncontrolled Components: Let the DOM Store the Current Value

Now look at the uncontrolled input box:

import { useRef, useState } from 'react'

function UncontrolledInput() {
  const inputRef = useRef(null)
  const [submittedValue, setSubmittedValue] = useState('')

  const handleRead = () => {
    setSubmittedValue(inputRef.current.value)
  }

  return (
    <>
      <label htmlFor="message">Message</label>
      <input id="message" ref={inputRef} />
      <button type="button" onClick={handleRead}>
        Read DOM value
      </button>
      <p>This read: {submittedValue || 'Not yet read'}</p>
    </>
  )
}

Here, no value is bound to the input box. After the user types, the content is directly stored in the DOM node. inputRef.current points to the real <input>, so the value can be read when the button is clicked:

inputRef.current.value

It's important to note: useRef stores a reference, but modifying ref.current does not trigger a component re-render.

During input, the DOM value is constantly changing, but React does not re-execute the component function because of this. The submittedValue in the example is only used to display "the last read result" on the page; it is not responsible for controlling the input box.

How to Set an Initial Value

If an uncontrolled component needs an initial value, use defaultValue:

<input ref={inputRef} defaultValue="Default nickname" />

defaultValue is only responsible for the initial content; subsequent input is still managed by the DOM. If you use value instead, the component enters controlled mode.

4. Comparing the Two Approaches Side by Side

Comparison Item Controlled Component Uncontrolled Component
Data storage location React state DOM node
Reading method Read state directly Via ref.current.value
Updates state on input Yes No
Real-time validation Easy to implement Usually handled on read or submit
Field linkage Easy to implement Requires manual reading and syncing
Resetting content Update state Manipulate DOM or reset native form
Integration with non-React code Requires state synchronization Usually more direct
Typical scenarios Login, registration, search filters, dynamic forms Simple submissions, file selection, integrating legacy pages

There is no absolute superiority here. Controlled components provide a clearer data flow, while uncontrolled components reduce state synchronization during input.

5. How Multiple Fields Share a Single Change Handler

Real business forms usually have more than one field. The registration form in the example uses a single object to manage username and password uniformly:

const initialForm = {
  username: '',
  password: '',
}

function RegisterForm() {
  const [form, setForm] = useState(initialForm)

  const handleChange = (event) => {
    const { name, value } = event.target

    setForm((previousForm) => ({
      ...previousForm,
      [name]: value,
    }))
  }

  return (
    <form>
      <input
        name="username"
        value={form.username}
        onChange={handleChange}
      />
      <input
        name="password"
        type="password"
        value={form.password}
        onChange={handleChange}
      />
    </form>
  )
}

The key is that the name of each input box matches the field name in state:

[name]: value

This is JavaScript's computed property name. When the username input box changes, name is username; when the password input box changes, name is password. This way, you don't need to write a separate handler function for each field.

A functional update is used here:

setForm((previousForm) => ({
  ...previousForm,
  [name]: value,
}))

It explicitly means "generate the next state based on the previous state," which is safer during consecutive updates or when logic continues to expand.

6. Why Controlled Forms Are Suitable for Real-Time Validation

Because the input content is already in state, we can validate immediately after each change:

function validate(form) {
  const errors = {}

  if (!form.username.trim()) {
    errors.username = 'Please enter a username'
  } else if (form.username.trim().length < 3) {
    errors.username = 'Username must be at least 3 characters'
  }

  if (!form.password) {
    errors.password = 'Please enter a password'
  } else if (form.password.length < 6) {
    errors.password = 'Password must be at least 6 characters'
  }

  return errors
}

Calculate the next form and error information during input:

const handleChange = (event) => {
  const { name, value } = event.target
  const nextForm = { ...form, [name]: value }

  setForm(nextForm)
  setErrors(validate(nextForm))
}

Perform a full validation again on submit:

const handleSubmit = (event) => {
  event.preventDefault()

  const nextErrors = validate(form)
  setErrors(nextErrors)

  if (Object.keys(nextErrors).length > 0) return

  console.log('Submit form', form)
}

Finally, the validation result can also control the button state:

const isValid = Object.keys(validate(form)).length === 0

<button type="submit" disabled={!isValid}>
  Submit Registration
</button>

This is the main value of controlled components: the same state simultaneously drives the input boxes, error messages, button states, and final submission data, making page behavior easier to reason about.

7. Don't Switch Back and Forth Between Controlled and Uncontrolled

The following approach is prone to problems:

const [value, setValue] = useState()

<input value={value} onChange={(e) => setValue(e.target.value)} />

The initial value is undefined, so the input box starts as effectively uncontrolled; after the user types, value becomes a string, and it switches to a controlled component.

If you plan to use a controlled input box, the initial value should also be a valid value of the corresponding type:

const [value, setValue] = useState('')

For checkboxes, you can use a boolean value:

const [agreed, setAgreed] = useState(false)

<input
  type="checkbox"
  checked={agreed}
  onChange={(event) => setAgreed(event.target.checked)}
/>

8. File Input Boxes Are a Typical Exception

For security reasons, browsers do not allow applications to arbitrarily set the value of a file input box. Therefore, file inputs are typically read in an uncontrolled manner:

function FilePicker() {
  const fileRef = useRef(null)

  const handleSubmit = () => {
    const file = fileRef.current.files[0]
    console.log(file)
  }

  return (
    <>
      <input ref={fileRef} type="file" />
      <button type="button" onClick={handleSubmit}>
        Upload
      </button>
    </>
  )
}

This also illustrates that real projects don't need to be forced into uniformity: the same form can have text fields controlled while reading files via ref.

9. How to Choose in Actual Development

Prioritize controlled components in the following situations:

Consider uncontrolled components in the following situations:

There's also a practical rule of thumb: if you frequently write ref.current.value and then sync the result into state, it's likely clearer to just use a controlled component directly.

10. Summary

The essence of controlled vs. uncontrolled components lies not in whether Hooks are used, but in who stores and determines the current value.

There is no fixed answer for form approaches. First, confirm when the data needs to be read and whether it participates in interface calculations, then decide whether to let React manage it or let the DOM temporarily hold it.