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.
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:
value={stateVariable} - field value is bound to stateonChange - every change updates state via setStateWhen a user types a character in a text field:
onChangesetStateThis is a closed loop - React controls every character typed into the form.
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:
ref={reference} - handle to the DOM elementdefaultValue instead of value - sets the initial value but doesn't control further changesref.current.value - reading value from the DOM when neededThe
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.| 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).
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.
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.