JavaScript and React course ยท Module 3: State and Events
Forms in React - Controlled Components
In this lesson6
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.
Controlled vs uncontrolled components
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 = (e) => {
5 e.preventDefault();
6 console.log(inputRef.current.value);
7 };
8
9 return (
10 <form onSubmit={handleSubmit}>
11 <input type="text" ref={inputRef} />
12 <button type="submit">Send</button>
13 </form>
14 );
15}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.
Handling text fields with useState
Simple text input
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}Managing multiple fields with a single state
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 and select elements
Checkbox - system toggles
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}Select - choosing from a list
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}Handling form submission
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}Lifting state up
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 // The parent only remembers what was typed last, and in which scale
16 const [temperature, setTemperature] = useState({ scale: "c", value: "" });
17
18 const convert = (value, formula) => {
19 const number = parseFloat(value);
20 return Number.isNaN(number) ? "" : String(Math.round(formula(number) * 100) / 100);
21 };
22
23 const celsius = temperature.scale === "f"
24 ? convert(temperature.value, (f) => ((f - 32) * 5) / 9)
25 : temperature.value;
26 const fahrenheit = temperature.scale === "c"
27 ? convert(temperature.value, (c) => (c * 9) / 5 + 32)
28 : temperature.value;
29
30 return (
31 <div className="converter">
32 <h2>Hull Temperature Converter</h2>
33 <TemperatureInput
34 label="Celsius"
35 value={celsius}
36 onChange={(value) => setTemperature({ scale: "c", value })}
37 />
38 <TemperatureInput
39 label="Fahrenheit"
40 value={fahrenheit}
41 onChange={(value) => setTemperature({ scale: "f", value })}
42 />
43 </div>
44 );
45}In this pattern:
- The state (
temperature: the last typed value and its scale) lives in the parent (TemperatureConverter) - The child components (
TemperatureInput) receive the value via props - Changes are passed upward through the
onChangecallback - The parent recalculates and synchronizes both values
The parent does not store ready-made degrees Celsius, but the last typed value and its scale. That way the field you are typing in shows exactly what you type (even a lone - or 10), and the other field is converted and rounded to two decimal places.
Summary
Forms in React - key principles:
- Controlled components - React manages input values through
value+onChange - Single state for multiple fields - use a state object with dynamic
[name]: value - Checkboxes - use
checkedinstead ofvalue, readevent.target.checked - Select - control via
valueon<select>, not on<option> - preventDefault() - always block the default form submission
- Validation - check data in the
onSubmithandler before submitting - Lifting state up - move state to a common parent when components need to share data
Code for this lesson: App.jsx
1import React, { useState } from 'react';
2
3function SpaceMissionForm() {
4 const [formData, setFormData] = useState({
5 missionName: '',
6 destination: 'mars',
7 crewSize: 4,
8 duration: '',
9 priority: 'medium',
10 equipment: [],
11 notes: '',
12 launchDate: '',
13 isUrgent: false
14 });
15
16 const [errors, setErrors] = useState({});
17 const [submitted, setSubmitted] = useState(false);
18
19 const equipmentOptions = [
20 { id: 'rover', label: 'Rover' },
21 { id: 'drone', label: 'Drone' },
22 { id: 'satellite', label: 'Satellite' },
23 { id: 'telescope', label: 'Telescope' },
24 { id: 'lab', label: 'Laboratory' }
25 ];
26
27 const handleChange = (e) => {
28 const { name, value, type, checked } = e.target;
29 setFormData(prev => ({
30 ...prev,
31 [name]: type === 'checkbox' ? checked : value
32 }));
33 if (errors[name]) {
34 setErrors(prev => ({ ...prev, [name]: null }));
35 }
36 };
37
38 const handleEquipmentChange = (id) => {
39 setFormData(prev => ({
40 ...prev,
41 equipment: prev.equipment.includes(id)
42 ? prev.equipment.filter(e => e !== id)
43 : [...prev.equipment, id]
44 }));
45 };
46
47 const validate = () => {
48 const newErrors = {};
49 if (!formData.missionName.trim()) newErrors.missionName = 'Mission name is required';
50 if (!formData.duration || formData.duration < 1) newErrors.duration = 'Duration must be greater than 0';
51 if (!formData.launchDate) newErrors.launchDate = 'Launch date is required';
52 if (formData.equipment.length === 0) newErrors.equipment = 'Select at least one piece of equipment';
53 return newErrors;
54 };
55
56 const handleSubmit = (e) => {
57 e.preventDefault();
58 const newErrors = validate();
59 if (Object.keys(newErrors).length > 0) {
60 setErrors(newErrors);
61 return;
62 }
63 setSubmitted(true);
64 console.log('Submitted:', formData);
65 };
66
67 const handleReset = () => {
68 setFormData({
69 missionName: '', destination: 'mars', crewSize: 4, duration: '',
70 priority: 'medium', equipment: [], notes: '', launchDate: '', isUrgent: false
71 });
72 setErrors({});
73 setSubmitted(false);
74 };
75
76 if (submitted) {
77 return (
78 <div style={{ fontFamily: 'Arial', padding: '40px', textAlign: 'center' }}>
79 <div style={{ fontSize: '64px', marginBottom: '20px' }}>โฒ</div>
80 <h1 style={{ color: '#4caf50' }}>Mission scheduled!</h1>
81 <div style={{ backgroundColor: '#f5f5f5', padding: '20px', borderRadius: '12px', maxWidth: '500px', margin: '20px auto', textAlign: 'left' }}>
82 <p><strong>Name:</strong> {formData.missionName}</p>
83 <p><strong>Destination:</strong> {formData.destination}</p>
84 <p><strong>Crew:</strong> {formData.crewSize} people</p>
85 <p><strong>Duration:</strong> {formData.duration} days</p>
86 <p><strong>Launch date:</strong> {formData.launchDate}</p>
87 <p><strong>Equipment:</strong> {formData.equipment.join(', ')}</p>
88 </div>
89 <button onClick={handleReset} style={{ padding: '12px 24px', backgroundColor: '#667eea', color: 'white', border: 'none', borderRadius: '8px', cursor: 'pointer' }}>
90 Schedule a new mission
91 </button>
92 </div>
93 );
94 }
95
96 return (
97 <div style={{ fontFamily: 'Arial', padding: '20px', maxWidth: '600px', margin: '0 auto' }}>
98 <h1 style={{ textAlign: 'center', color: '#667eea' }}>Space Mission Planning</h1>
99
100 <form onSubmit={handleSubmit} style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
101 <div>
102 <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>Mission name *</label>
103 <input type="text" name="missionName" value={formData.missionName} onChange={handleChange}
104 style={{ width: '100%', padding: '10px', borderRadius: '8px', border: errors.missionName ? '2px solid #f44336' : '1px solid #ddd', boxSizing: 'border-box' }}
105 />
106 {errors.missionName && <span style={{ color: '#f44336', fontSize: '12px' }}>{errors.missionName}</span>}
107 </div>
108
109 <div>
110 <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>Travel destination</label>
111 <select name="destination" value={formData.destination} onChange={handleChange}
112 style={{ width: '100%', padding: '10px', borderRadius: '8px', border: '1px solid #ddd' }}>
113 <option value="mars">Mars</option>
114 <option value="moon">Moon</option>
115 <option value="venus">Venus</option>
116 <option value="jupiter">Jupiter</option>
117 <option value="saturn">Saturn</option>
118 </select>
119 </div>
120
121 <div>
122 <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>Crew size: {formData.crewSize}</label>
123 <input type="range" name="crewSize" min="1" max="20" value={formData.crewSize} onChange={handleChange}
124 style={{ width: '100%' }}
125 />
126 </div>
127
128 <div>
129 <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>Duration (days) *</label>
130 <input type="number" name="duration" value={formData.duration} onChange={handleChange}
131 style={{ width: '100%', padding: '10px', borderRadius: '8px', border: errors.duration ? '2px solid #f44336' : '1px solid #ddd', boxSizing: 'border-box' }}
132 />
133 {errors.duration && <span style={{ color: '#f44336', fontSize: '12px' }}>{errors.duration}</span>}
134 </div>
135
136 <div>
137 <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>Launch date *</label>
138 <input type="date" name="launchDate" value={formData.launchDate} onChange={handleChange}
139 style={{ width: '100%', padding: '10px', borderRadius: '8px', border: errors.launchDate ? '2px solid #f44336' : '1px solid #ddd', boxSizing: 'border-box' }}
140 />
141 {errors.launchDate && <span style={{ color: '#f44336', fontSize: '12px' }}>{errors.launchDate}</span>}
142 </div>
143
144 <div>
145 <label style={{ display: 'block', marginBottom: '10px', fontWeight: 'bold' }}>Priority</label>
146 <div style={{ display: 'flex', gap: '20px' }}>
147 {['low', 'medium', 'high'].map(p => (
148 <label key={p} style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
149 <input type="radio" name="priority" value={p} checked={formData.priority === p} onChange={handleChange} />
150 {p === 'low' ? 'Low' : p === 'medium' ? 'Medium' : 'High'}
151 </label>
152 ))}
153 </div>
154 </div>
155
156 <div>
157 <label style={{ display: 'block', marginBottom: '10px', fontWeight: 'bold' }}>Equipment *</label>
158 <div style={{ display: 'flex', flexWrap: 'wrap', gap: '10px' }}>
159 {equipmentOptions.map(eq => (
160 <label key={eq.id} style={{
161 padding: '8px 16px', borderRadius: '20px', cursor: 'pointer',
162 backgroundColor: formData.equipment.includes(eq.id) ? '#667eea' : '#e0e0e0',
163 color: formData.equipment.includes(eq.id) ? 'white' : '#333'
164 }}>
165 <input type="checkbox" checked={formData.equipment.includes(eq.id)}
166 onChange={() => handleEquipmentChange(eq.id)} style={{ display: 'none' }}
167 />
168 {eq.label}
169 </label>
170 ))}
171 </div>
172 {errors.equipment && <span style={{ color: '#f44336', fontSize: '12px' }}>{errors.equipment}</span>}
173 </div>
174
175 <div>
176 <label style={{ display: 'flex', alignItems: 'center', gap: '10px', cursor: 'pointer' }}>
177 <input type="checkbox" name="isUrgent" checked={formData.isUrgent} onChange={handleChange} />
178 <span style={{ fontWeight: 'bold' }}>Urgent mission</span>
179 </label>
180 </div>
181
182 <div>
183 <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>Notes</label>
184 <textarea name="notes" value={formData.notes} onChange={handleChange} rows="3"
185 style={{ width: '100%', padding: '10px', borderRadius: '8px', border: '1px solid #ddd', boxSizing: 'border-box', resize: 'vertical' }}
186 />
187 </div>
188
189 <div style={{ display: 'flex', gap: '10px' }}>
190 <button type="submit" style={{ flex: 1, padding: '12px', backgroundColor: '#4caf50', color: 'white', border: 'none', borderRadius: '8px', cursor: 'pointer', fontWeight: 'bold' }}>
191 Schedule mission
192 </button>
193 <button type="button" onClick={handleReset} style={{ flex: 1, padding: '12px', backgroundColor: '#f44336', color: 'white', border: 'none', borderRadius: '8px', cursor: 'pointer', fontWeight: 'bold' }}>
194 Clear
195 </button>
196 </div>
197 </form>
198 </div>
199 );
200}
201
202export default function App() {
203 return <SpaceMissionForm />;
204}Remember: Controlled components are like the ship's control system - the onboard computer (React) knows about every change in real time and can react immediately!
Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. Can you use multiple useState hooks in a single component?
2. What is the difference between state and props?
These are 2 of 9 questions for this lesson. Solve the rest in the game.
Hands-on tasks in the game
- Click in order
Build an onSubmit handler that prevents page reload:
- Vertical ordering
Arrange the steps for creating a controlled form in React:
- Code editor
Create an EmergencyAlert component with a message state shown in a <p> paragraph and render it in App. Add an ALARM button: onClick sets the message 'Alarm activated!' and onMouseEnter sets 'Careful: cursor over the alarm button'. Also add an <input> field with onKeyDown that sets 'Alarm triggered from the keyboard' when Enter is pressed (e.key === 'Enter') and ignores other keys.
- Code editor
Create a parent ColonyControl with a powerLevel state (useState(50)) and two children without their own state: PowerPlant({ powerLevel, onPowerChange }) shows the power level and has +10 and -10 buttons that call onPowerChange with the new value, and LifeSupport({ powerLevel }), which shows 'Life support: active' when powerLevel >= 30 and 'ALARM: not enough power' otherwise. ColonyControl passes the same powerLevel to both children. Render ColonyControl in App.
- Code editor
Create a ColonyDashboard component with four separate useState states: temperature (e.g. -20), pressure (e.g. 600), oxygen (e.g. 21) and power (e.g. 80), and render it in App. Display all four values and, for each of them, add a button that changes only that value (e.g. Heat +5, Pressure +10, Oxygen +1, Power -10).
- Click in order
Build the expression that retrieves the value from an input field in an event handler:
- Horizontal ordering
Arrange the syntax for importing the useState hook from React:
- Click in order
Arrange the event handling flow:
- Vertical ordering
Arrange the ways of keeping state from the simplest to the most elaborate, as in the lesson on data flow:
- Vertical ordering
Arrange what happens after typing one character into the controlled field <input value={name} onChange={(e) => setName(e.target.value)} />:
- Click in order
Arrange the state update process:
- Horizontal ordering
Arrange the correct onChange attribute for a controlled input:
- Click in order
Click the elements in order to create a controlled input with onChange:
- Vertical ordering
Arrange the steps of the React state update cycle from event to DOM update:
- Horizontal ordering
Arrange the correct way to update a single property of an object in useState:
- Vertical ordering
Arrange the validation steps of the MissionPlanner form from the lesson on event handling: