We use cookies to enhance your experience on the site
CodeWorlds

Controlled vs uncontrolled components

On a space station, every system operates in one of two modes: either the command center has full control over the parameters (manual mode), or the system operates autonomously and the center only reads data when needed (automatic mode). In React, forms and input fields work analogously - they can be controlled by component state or uncontrolled, where the browser manages their value.

Controlled Component

A controlled component is one where React is the "single source of truth" for the form field value. Every value change goes through the component state:

1import React, { useState } from 'react';
2
3function PilotLogin() {
4  const [pilotName, setPilotName] = useState('');
5  const [accessCode, setAccessCode] = useState('');
6
7  const handleSubmit = (e) => {
8    e.preventDefault();
9    console.log(`Pilot login: ${pilotName}, code: ${accessCode}`);
10  };
11
12  return (
13    <form onSubmit={handleSubmit}>
14      <label>
15        Pilot name:
16        <input
17          type="text"
18          value={pilotName}
19          onChange={(e) => setPilotName(e.target.value)}
20        />
21      </label>
22      <label>
23        Access code:
24        <input
25          type="password"
26          value={accessCode}
27          onChange={(e) => setAccessCode(e.target.value)}
28        />
29      </label>
30      <button type="submit">Log in</button>
31    </form>
32  );
33}

Key elements of a controlled component:

  1. value={stateVariable}
    - field value is bound to state
  2. onChange
    - every change updates state via
    setState
  3. React state is the single source of truth - the browser displays what React tells it to

Data flow in a controlled component

When a user types a character in a text field:

  1. The browser detects the change and fires
    onChange
  2. The handler updates state via
    setState
  3. React re-renders the component with the new value
  4. The field displays the new value from state

This is a closed loop - React controls every character typed into the form.

Uncontrolled Component

In an uncontrolled component, the browser (DOM) manages the field value. React doesn't track every change - instead, it reads the value only when it needs it (e.g., when submitting a form). To read values from the DOM, we use ref (references):

1import React, { useRef } from 'react';
2
3function QuickSearch() {
4  const searchInputRef = useRef(null);
5
6  const handleSearch = (e) => {
7    e.preventDefault();
8    // We read the value directly from the DOM
9    const query = searchInputRef.current.value;
10    console.log(`Searching: ${query}`);
11  };
12
13  return (
14    <form onSubmit={handleSearch}>
15      <input
16        type="text"
17        ref={searchInputRef}
18        defaultValue=""
19        placeholder="Search the station database..."
20      />
21      <button type="submit">Search</button>
22    </form>
23  );
24}

Key elements of an uncontrolled component:

  1. ref={reference}
    - handle to the DOM element
  2. defaultValue
    instead of
    value
    - sets the initial value but doesn't control further changes
  3. ref.current.value
    - reading value from the DOM when needed

useRef - accessing DOM elements

The

useRef
hook creates an object with a
current
property that points to the DOM element after the component mounts:

1import React, { useRef } from 'react';
2
3function FocusInput() {
4  const inputRef = useRef(null);
5
6  const handleClick = () => {
7    // Direct access to DOM element
8    inputRef.current.focus();
9    inputRef.current.style.borderColor = '#4caf50';
10  };
11
12  return (
13    <div>
14      <input ref={inputRef} type="text" placeholder="Click the button..." />
15      <button onClick={handleClick}>Set focus</button>
16    </div>
17  );
18}

useRef
has an important characteristic: changing
ref.current
does not cause a re-render of the component. This makes it ideal for storing references to DOM elements without impacting performance.

When to use controlled vs uncontrolled?

| Scenario | Controlled | Uncontrolled | |----------|:---:|:---:| | Real-time validation (every character) | yes | - | | Formatting data on the fly (e.g., card number) | yes | - | | Conditionally disabling the submit button | yes | - | | Dynamic fields dependent on each other | yes | - | | Simple "submit and forget" form | - | yes | | Integration with external code (non-React) | - | yes | | Maximum performance (no re-renders per keystroke) | - | yes |

General rule: use controlled components by default. Use uncontrolled ones only when you have a specific reason (performance, integration with an external library, file upload).

Form validation with a controlled component

A great advantage of controlled components is the ability to validate data in real time:

1function CrewRegistration() {
2  const [name, setName] = useState('');
3  const [age, setAge] = useState('');
4  const [errors, setErrors] = useState({});
5
6  const validate = () => {
7    const newErrors = {};
8    if (name.length < 2) newErrors.name = 'Name must be at least 2 characters';
9    if (!age || age < 18 || age > 65) newErrors.age = 'Age: 18-65 years';
10    return newErrors;
11  };
12
13  const handleSubmit = (e) => {
14    e.preventDefault();
15    const newErrors = validate();
16    if (Object.keys(newErrors).length === 0) {
17      console.log(`Registration: ${name}, age: ${age}`);
18    } else {
19      setErrors(newErrors);
20    }
21  };
22
23  return (
24    <form onSubmit={handleSubmit}>
25      <div>
26        <input
27          value={name}
28          onChange={(e) => setName(e.target.value)}
29          placeholder="Crew member name"
30        />
31        {errors.name && <span style={{color: 'red'}}>{errors.name}</span>}
32      </div>
33      <div>
34        <input
35          type="number"
36          value={age}
37          onChange={(e) => setAge(e.target.value)}
38          placeholder="Age"
39        />
40        {errors.age && <span style={{color: 'red'}}>{errors.age}</span>}
41      </div>
42      <button type="submit">Register</button>
43    </form>
44  );
45}

In a controlled component, we can continuously check data, display error messages, and conditionally enable the submit button - this is impossible in an uncontrolled component without additional code.

Summary

Controlled components give you full power over the form - React tracks every value change, enabling real-time validation, formatting, and dynamic behavior. Uncontrolled components are simpler - the browser manages the value, and React reads it through ref only when needed. In most cases, controlled components are the better choice, but uncontrolled ones have their place in simple forms and integrations.

Go to CodeWorlds