Next.js

Fix "changing an uncontrolled input to be controlled"

Why an empty state object makes React treat a text field as uncontrolled, what the React 19 wording looks like, and the react-hook-form variant of the same warning.

Mahdi Ben RhoumaMahdi Ben Rhouma
7 min read

TL;DR#

The input received value={undefined} on its first render, so React treated it as uncontrolled. A later render passed a string, which makes it controlled, and React warns when one input switches modes. Make the value a string from the very first render: initialise the state key to an empty string, or write value={fields.name ?? ''}. If the field really should be uncontrolled, drop value and use defaultValue instead.

Current React (19.3.1, development build) prints:

text
console.error: A component is changing an uncontrolled input to be controlled. This is likely caused by the value changing from undefined to a defined value, which should not happen. Decide between using a controlled or uncontrolled input element for the lifetime of the component. More info: https://react.dev/link/controlled-components

The console.error: prefix comes from the small harness used for the reproduction below, not from React. Older projects show the wording from the original Stack Overflow question, which names the input type:

text
Warning: A component is changing an uncontrolled input of type text to be controlled. Input elements should not switch from uncontrolled to controlled (or vice versa). Decide between using a controlled or uncontrolled input element for the lifetime of the component.

Both strings describe the same event. The rest of this page applies to either.

Root cause: React decides on the first render#

React classifies a text input by its value prop: a string makes it controlled, while undefined (and, in practice, null) leaves it uncontrolled. The react.dev reference for the input component puts it plainly: a controlled component should always receive a string value, never null or undefined, because React cannot tell whether value={undefined} followed by value="some string" means you wanted a controlled or an uncontrolled field.

The code in the question hits this exactly. The class component starts with an empty object:

jsx
this.state = {
  fields: {},
  errors: {}
};

and renders the input as:

jsx
<input
  value={this.state.fields["name"]}
  onChange={this.onChange.bind(this, "name")}
  type="text"
/>

On the first render this.state.fields["name"] does not exist, so the input gets value={undefined} and React leaves the DOM in charge. The first keystroke runs onChange, which stores e.target.value under name. The next render passes a real string, and the same input is now controlled. That transition is what the warning reports. The accepted answer gives this same diagnosis.

Hooks behave identically. useState({}) followed by value={fields.name} produces the same sequence, and so does any value that arrives later from a fetch, a context, or a parent prop that starts as undefined.

Fix: give the input a string before the first render#

Pick the variant that matches where the value comes from. Each one changes a single thing.

  1. Initialise every key the form renders. When the state shape is known up front, declare it:
jsx
import { useState } from 'react';
 
export function NameForm() {
  const [fields, setFields] = useState({ name: '' });
 
  return (
    <input
      type="text"
      value={fields.name}
      onChange={(e) => setFields({ ...fields, name: e.target.value })}
    />
  );
}

In a class component the equivalent is this.state = { fields: { name: '' }, errors: {} }.

  1. Fall back at the prop when the key may be missing. Data loaded from an API, a dynamic list of fields, or a parent that renders before its data arrives cannot always be pre-initialised. Coerce at the input instead:
jsx
<input
  type="text"
  value={fields.name ?? ''}
  onChange={(e) => setFields({ ...fields, name: e.target.value })}
/>

The react.dev reference recommends the same ?? '' form. If the data comes from an effect, the cancellation and double-run behaviour of that effect is a separate topic: see the useEffect running twice fix for Strict Mode and the unmounted-component state update fix for cancelling a request when the form unmounts.

  1. Or make the field genuinely uncontrolled. If nothing reads the value until submit, do not pass value at all. Give the initial text through defaultValue and read the field from the form data on submit. React then never sees a switch, because the input never receives value.
jsx
export function NameForm() {
  function handleSubmit(e) {
    e.preventDefault();
    const name = new FormData(e.currentTarget).get('name');
    console.log(name);
  }
 
  return (
    <form onSubmit={handleSubmit}>
      <input type="text" name="name" defaultValue="" />
      <button type="submit">Save</button>
    </form>
  );
}

React's docs say that defaultValue only specifies the initial value. Choose this route when you do not need the field's current value during render.

Verify the fix#

The behaviour was reproduced on 30 October 2026 with React 19.3.1 and react-dom 19.3.1 (development build), rendered into jsdom 30.1.3 on Node v24.22.2 in the official node:24-alpine image. A small script renders the input, then updates the state from outside the component instead of simulating a keystroke; in the question, the first keystroke plays that role. The script repro.mjs takes a variant name. With the state starting as an empty object, the warning fires:

text
[broken] first render: input.value=""
console.error: A component is changing an uncontrolled input to be controlled. This is likely caused by the value changing from undefined to a defined value, which should not happen. Decide between using a controlled or uncontrolled input element for the lifetime of the component. More info: https://react.dev/link/controlled-components
[broken] after update: input.value="Zoë"

With only the fallback added (fields.name ?? '', the fixed-fallback variant), the same update is silent:

text
[fixed-fallback] first render: input.value=""
[fixed-fallback] after update: input.value="Zoë"
react 19.3.1

Initialising the state to { name: '' } (node repro.mjs fixed-empty-string) is equally silent. In your own app the check is the same: reload the page with the browser console open, type a character into the field, and confirm that no warning appears. Check in development mode (next dev, vite), as the reproduction did with the react-dom development build.

One email a month — the latest explained errors

What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.

The reverse warning, null, and the || trap#

Three neighbours of this warning came out of the same reproduction.

Controlled to uncontrolled. Setting a controlled input's value back to undefined, for example after setName(undefined) to "clear" a field, triggers the mirror warning:

text
console.error: A component is changing a controlled input to be uncontrolled. This is likely caused by the value changing from a defined to undefined, which should not happen. Decide between using a controlled or uncontrolled input element for the lifetime of the component. More info: https://react.dev/link/controlled-components

In that run the field kept showing its old text after the reset, so the "clear" did not clear anything. Reset to an empty string instead.

Starting with null. useState(null) does not avoid the problem. React first prints a separate warning about null, and then still reports the switch once the value becomes a string:

text
console.error: `value` prop on `input` should not be null. Consider using an empty string to clear the component or `undefined` for uncontrolled components.

|| versus ??. Several answers on the source question use value={this.state.fields.name || ''}. That stops the warning, but it also erases legitimate falsy values. With a quantity field initialised to 0, the || version rendered an empty field, while the ?? version kept the zero:

text
[zero-or or] first render: input.value=""
[zero-or nullish] first render: input.value="0"

Prefer ?? unless you really want 0 shown as blank.

Which React prints which wording#

The wording you see depends on the react-dom version, which is why searches for "of type text" still find old answers. The development bundle in the published react-dom 16.14.0 tarball contains the template A component is changing an uncontrolled input of type %s to be controlled, where %s becomes the input type. The development bundle in the react-dom 17.0.2 tarball already uses the current message without the type. The reproduction on React 18.3.1 printed the current message with a Warning: prefix and a reactjs.org link. On React 19.3.1 it printed the same message with no prefix and a react.dev link. The fix is identical across all of them.

An undefined prop is also behind a different React error. If the console says a callback "is not a function" rather than warning about the input, a parent and child disagree on a prop name, which is covered in the props.handleSelect is not a function fix.

react-hook-form and shadcn forms#

react-hook-form setups print the same warning, as in this shadcn Form question and this Material UI TextField question. shadcn's form component source renders its FormField as a react-hook-form Controller. The mechanism is the same: with no default value, field.value is undefined on the first render, and spreading field onto an <input> starts it uncontrolled. The react-hook-form docs for useForm say to avoid undefined as a default value, and the Controller troubleshooting section tells you to give the field a default value in useForm, even an empty string.

This was reproduced with react-hook-form 7.91.0 on React 19.3.1 by rendering a Controller and calling setValue('firstName', 'Zoë'). Without defaultValues the warning fired. With only defaultValues: { firstName: '' } added, it did not:

jsx
import { useForm, Controller } from 'react-hook-form';
 
export function ProfileForm() {
  const { control } = useForm({ defaultValues: { firstName: '' } });
 
  return (
    <Controller
      name="firstName"
      control={control}
      render={({ field }) => <input type="text" {...field} />}
    />
  );
}
text
[rhf with-defaults] first render: input.value=""
[rhf with-defaults] after setValue: input.value="Zoë"

Declare a default for every field the form renders, not only the ones you remember to prefill. For validation rules on top of these fields, the confirm-password validation page shows the validate callback pattern. If the inputs live in a list, key them by a stable id, because index keys move uncontrolled input state between rows.

The rule underneath every variant is short: decide on the first render whether React or the DOM owns the field, and keep that decision. For a controlled field, that means a string on the first render and a string on every render after it, whether the value comes from useState, a class component's state, a fetch, or a form library's defaultValues.

Frequently Asked Questions

|

Have more questions? Contact us

Still stuck? Paste your error.

Paste the error message or stack trace — we check it against every fix and tool on the site.

Matching happens in your browser. When you search, a cleaned error signature is sent so we can spot errors we do not cover yet — never the raw text, your IP address or your e-mail. Do not paste passwords or keys. Details

Written by

Mahdi Ben Rhouma
Mahdi Ben Rhouma

Full-Stack Dev — Next.js & Supabase

Solo developer building SaaS products with Next.js and Supabase. Writing about production patterns the official docs skip.

Remote