On board the React spaceship, forms are like configuration panels - they serve to input navigation data, set mission parameters, and communicate with command headquarters. In this lesson, you will learn about controlled components - a pattern where React takes full control of form data, just like the onboard computer controls every ship system.
In React, we distinguish two approaches to form handling:
Controlled components - React manages the input value through state. Every change goes through
useState and onChange:1function ControlledInput() {
2 const [name, setName] = useState("");
3
4 return (
5 <input
6 type="text"
7 value={name}
8 onChange={(e) => setName(e.target.value)}
9 />
10 );
11}Uncontrolled components - the browser manages the value, and we read it via
ref:1function UncontrolledInput() {
2 const inputRef = useRef(null);
3
4 const handleSubmit = () => {
5 console.log(inputRef.current.value);
6 };
7
8 return <input type="text" ref={inputRef} />;
9}Why are controlled components better? Just as the ship's onboard computer must know the state of every system at all times, controlled components give React full visibility into the form data. This allows you to validate, format, and react to changes in real time.
1function PilotRegistration() {
2 const [pilotName, setPilotName] = useState("");
3 const [callSign, setCallSign] = useState("");
4
5 return (
6 <div className="registration-form">
7 <h2>Pilot Registration</h2>
8
9 <label>Pilot Name:</label>
10 <input
11 type="text"
12 value={pilotName}
13 onChange={(e) => setPilotName(e.target.value)}
14 placeholder="Enter name..."
15 />
16
17 <label>Call Sign:</label>
18 <input
19 type="text"
20 value={callSign}
21 onChange={(e) => setCallSign(e.target.value.toUpperCase())}
22 placeholder="E.g. ALPHA-7"
23 />
24
25 <p>Pilot: {pilotName} ({callSign})</p>
26 </div>
27 );
28}Instead of creating a separate
useState for each field, you can use a single state object - like a central mission data registry:1function MissionConfigForm() {
2 const [formData, setFormData] = useState({
3 missionName: "",
4 destination: "",
5 crewSize: "",
6 priority: "normal"
7 });
8
9 const handleChange = (event) => {
10 const { name, value } = event.target;
11 setFormData((prev) => ({
12 ...prev,
13 [name]: value
14 }));
15 };
16
17 return (
18 <div className="mission-config">
19 <h2>Mission Configuration</h2>
20
21 <label>Mission Name:</label>
22 <input
23 type="text"
24 name="missionName"
25 value={formData.missionName}
26 onChange={handleChange}
27 />
28
29 <label>Destination:</label>
30 <input
31 type="text"
32 name="destination"
33 value={formData.destination}
34 onChange={handleChange}
35 />
36
37 <label>Crew Size:</label>
38 <input
39 type="number"
40 name="crewSize"
41 value={formData.crewSize}
42 onChange={handleChange}
43 />
44
45 <label>Priority:</label>
46 <input
47 type="text"
48 name="priority"
49 value={formData.priority}
50 onChange={handleChange}
51 />
52
53 <pre>{JSON.stringify(formData, null, 2)}</pre>
54 </div>
55 );
56}The key trick: the
name attribute on inputs corresponds to keys in the state object, and [name]: value (computed property name) dynamically updates the corresponding field.Checkboxes in React use
checked instead of value:1function ShipSystemsPanel() {
2 const [systems, setSystems] = useState({
3 engines: true,
4 shields: false,
5 weapons: false,
6 lifeSupport: true
7 });
8
9 const handleToggle = (event) => {
10 const { name, checked } = event.target;
11 setSystems((prev) => ({
12 ...prev,
13 [name]: checked
14 }));
15 };
16
17 return (
18 <div className="systems-panel">
19 <h2>Ship Systems</h2>
20
21 <label>
22 <input
23 type="checkbox"
24 name="engines"
25 checked={systems.engines}
26 onChange={handleToggle}
27 />
28 Engines
29 </label>
30
31 <label>
32 <input
33 type="checkbox"
34 name="shields"
35 checked={systems.shields}
36 onChange={handleToggle}
37 />
38 Shields
39 </label>
40
41 <label>
42 <input
43 type="checkbox"
44 name="weapons"
45 checked={systems.weapons}
46 onChange={handleToggle}
47 />
48 Weapon Systems
49 </label>
50
51 <label>
52 <input
53 type="checkbox"
54 name="lifeSupport"
55 checked={systems.lifeSupport}
56 onChange={handleToggle}
57 />
58 Life Support
59 </label>
60
61 <div className="status">
62 Active systems: {Object.entries(systems)
63 .filter(([, active]) => active)
64 .map(([name]) => name)
65 .join(", ")}
66 </div>
67 </div>
68 );
69}The
<select> element in React works like other controlled inputs - we control the value through value and onChange:1function NavigationPanel() {
2 const [destination, setDestination] = useState("");
3 const [warpSpeed, setWarpSpeed] = useState("1");
4
5 const destinations = [
6 { value: "mars", label: "Mars" },
7 { value: "jupiter", label: "Jupiter" },
8 { value: "saturn", label: "Saturn" },
9 { value: "alpha-centauri", label: "Alpha Centauri" },
10 { value: "andromeda", label: "Andromeda Galaxy" }
11 ];
12
13 return (
14 <div className="nav-panel">
15 <h2>Navigation Panel</h2>
16
17 <label>Destination:</label>
18 <select
19 value={destination}
20 onChange={(e) => setDestination(e.target.value)}
21 >
22 <option value="">-- Select destination --</option>
23 {destinations.map((dest) => (
24 <option key={dest.value} value={dest.value}>
25 {dest.label}
26 </option>
27 ))}
28 </select>
29
30 <label>Warp Speed:</label>
31 <select
32 value={warpSpeed}
33 onChange={(e) => setWarpSpeed(e.target.value)}
34 >
35 <option value="1">Warp 1</option>
36 <option value="3">Warp 3</option>
37 <option value="5">Warp 5</option>
38 <option value="9">Warp 9 (maximum)</option>
39 </select>
40
41 {destination && (
42 <p>Course set: {destination} at Warp {warpSpeed}</p>
43 )}
44 </div>
45 );
46}Submitting a form is like issuing an order from command headquarters - it must be controlled and secure:
1function MissionReportForm() {
2 const [formData, setFormData] = useState({
3 missionName: "",
4 commander: "",
5 status: "in-progress",
6 description: "",
7 isClassified: false
8 });
9 const [submitted, setSubmitted] = useState(false);
10
11 const handleChange = (event) => {
12 const { name, value, type, checked } = event.target;
13 setFormData((prev) => ({
14 ...prev,
15 [name]: type === "checkbox" ? checked : value
16 }));
17 };
18
19 const handleSubmit = (event) => {
20 event.preventDefault();
21
22 // Validation
23 if (!formData.missionName.trim() || !formData.commander.trim()) {
24 alert("Please fill in all required fields!");
25 return;
26 }
27
28 console.log("Mission report:", formData);
29 setSubmitted(true);
30 };
31
32 const handleReset = () => {
33 setFormData({
34 missionName: "",
35 commander: "",
36 status: "in-progress",
37 description: "",
38 isClassified: false
39 });
40 setSubmitted(false);
41 };
42
43 if (submitted) {
44 return (
45 <div className="success">
46 <h2>Report Submitted!</h2>
47 <p>Mission: {formData.missionName}</p>
48 <button onClick={handleReset}>New Report</button>
49 </div>
50 );
51 }
52
53 return (
54 <form onSubmit={handleSubmit} className="mission-report">
55 <h2>Mission Report</h2>
56
57 <label>Mission Name *:</label>
58 <input
59 type="text"
60 name="missionName"
61 value={formData.missionName}
62 onChange={handleChange}
63 required
64 />
65
66 <label>Commander *:</label>
67 <input
68 type="text"
69 name="commander"
70 value={formData.commander}
71 onChange={handleChange}
72 required
73 />
74
75 <label>Status:</label>
76 <select name="status" value={formData.status} onChange={handleChange}>
77 <option value="in-progress">In Progress</option>
78 <option value="completed">Completed</option>
79 <option value="failed">Failed</option>
80 </select>
81
82 <label>Description:</label>
83 <textarea
84 name="description"
85 value={formData.description}
86 onChange={handleChange}
87 rows={4}
88 placeholder="Describe the mission progress..."
89 />
90
91 <label>
92 <input
93 type="checkbox"
94 name="isClassified"
95 checked={formData.isClassified}
96 onChange={handleChange}
97 />
98 Classified Report
99 </label>
100
101 <div className="form-actions">
102 <button type="submit">Submit Report</button>
103 <button type="button" onClick={handleReset}>Clear</button>
104 </div>
105 </form>
106 );
107}When multiple components need to share form data, we move the state to their common parent - like a command center coordinating data from multiple panels:
1function TemperatureInput({ label, value, onChange }) {
2 return (
3 <div>
4 <label>{label}:</label>
5 <input
6 type="number"
7 value={value}
8 onChange={(e) => onChange(e.target.value)}
9 />
10 </div>
11 );
12}
13
14function TemperatureConverter() {
15 const [celsius, setCelsius] = useState("");
16
17 const handleCelsiusChange = (value) => {
18 setCelsius(value);
19 };
20
21 const handleFahrenheitChange = (value) => {
22 const c = ((parseFloat(value) - 32) * 5) / 9;
23 setCelsius(isNaN(c) ? "" : c.toFixed(2));
24 };
25
26 const fahrenheit = celsius !== ""
27 ? ((parseFloat(celsius) * 9) / 5 + 32).toFixed(2)
28 : "";
29
30 return (
31 <div className="converter">
32 <h2>Hull Temperature Converter</h2>
33 <TemperatureInput
34 label="Celsius"
35 value={celsius}
36 onChange={handleCelsiusChange}
37 />
38 <TemperatureInput
39 label="Fahrenheit"
40 value={fahrenheit}
41 onChange={handleFahrenheitChange}
42 />
43 </div>
44 );
45}In this pattern:
celsius) lives in the parent (TemperatureConverter)TemperatureInput) receive the value via propsonChange callbackForms in React - key principles:
value + onChange[name]: valuechecked instead of value, read event.target.checkedvalue on <select>, not on <option>onSubmit handler before submittingRemember: Controlled components are like the ship's control system - the onboard computer (React) knows about every change in real time and can react immediately!