▶️ Live demo
Try it yourself — interact with the example below.
Loading demo…
🎯 What Are Controlled Components?
Imagine you're driving a car with two steering wheels:
UNCONTROLLED (DOM Owns the State):
┌─────────────────────────────────────────┐
│ 🚗 CAR WITH TWO STEERING WHEELS │
│ │
│ Wheel 1: React (you think you're driving)│
│ Wheel 2: DOM (actually driving itself) │
│ │
│ You turn your wheel left... │
│ But DOM wheel stays straight! │
│ Car goes straight! You crash! │
│ │
│ In React: │
│ <input type="text" /> │
│ // DOM controls the value! │
│ // React has NO IDEA what's typed! │
│ │
│ Problem: │
│ • Can't read the value easily │
│ • State lives in DOM, not React │
│ • React and DOM are out of sync! │
└─────────────────────────────────────────┘
CONTROLLED (React Owns the State):
┌─────────────────────────────────────────┐
│ 🚗 CAR WITH ONE STEERING WHEEL │
│ │
│ Wheel: React (you're in full control) │
│ │
│ You turn wheel left... │
│ Car goes left! Perfect! │
│ │
│ In React: │
│ const [value, setValue] = useState("");│
│ <input value={value} onChange={...} /> │
│ // React controls the value! │
│ // DOM just displays what React says! │
│ │
│ Benefits: │
│ • Value is in React state │
│ • Easy to read, validate, reset │
│ • React and DOM always in sync! │
└─────────────────────────────────────────┘
THE 3-STEP TECHNIQUE:
┌─────────────────────────────────────────┐
│ Step 1: Create state │
│ const [text, setText] = useState(""); │
│ │
│ Step 2: Use state as value │
│ <input value={text} /> │
│ │
│ Step 3: Update state on change │
│ <input onChange={e => setText(e.target.value)} />│
│ │
│ Result: React OWNS the input! │
└─────────────────────────────────────────┘
⚠️ The Big Problem: "React Can't See What User Types"
// ==========================================
// THE "INVISIBLE TYPING" TRAP
// ==========================================
// ❌ WRONG: Uncontrolled input (DOM owns state)
function BadForm() {
function handleSubmit(e) {
e.preventDefault();
// How do we get the value? 🤔
// We have to manually query the DOM!
const input = document.querySelector('input');
console.log(input.value); // Works but ugly!
}
return (
<form onSubmit={handleSubmit}>
<input type="text" placeholder="Item..." />
{/* React has NO IDEA what's in this input! */}
<button>Add</button>
</form>
);
}
// Problems:
// 1. React can't read the value easily
// 2. Can't reset the input by changing state
// 3. Can't validate in real-time
// 4. State is scattered (some in React, some in DOM)
// 5. Hard to keep multiple inputs in sync
// ==========================================
// THE SOLUTION: Controlled Component (3 Steps)
// ==========================================
// ✅ CORRECT: React owns the state
function GoodForm() {
// Step 1: Create state for the input
const [description, setDescription] = useState("");
function handleSubmit(e) {
e.preventDefault();
// Easy! Value is right here in state!
console.log(description); // ✨ Clean!
}
return (
<form onSubmit={handleSubmit}>
{/*
Step 2: Use state as value
Step 3: Update state on change
*/}
<input
type="text"
placeholder="Item..."
value={description} // ← Step 2
onChange={e => setDescription(e.target.value)} // ← Step 3
/>
<button>Add</button>
</form>
);
}
// What happens when user types "Socks":
//
// 1. User presses 'S'
// → onChange fires
// → e.target.value = "S"
// → setDescription("S")
// → state updates to "S"
// → React re-renders
// → input value = "S" ✓
//
// 2. User presses 'o'
// → onChange fires
// → e.target.value = "So"
// → setDescription("So")
// → state updates to "So"
// → React re-renders
// → input value = "So" ✓
//
// 3. User presses 'c', 'k', 's'
// → Same process repeats
// → Final state: "Socks"
// → Input shows: "Socks" ✓
//
// React ALWAYS knows the value!
// React ALWAYS controls what's displayed!
📋 Complete Visual Examples
Create file: react-controlled-components.js
// ==========================================
// CONTROLLED COMPONENTS - Complete Guide
// ==========================================
/*
THE 3-STEP TECHNIQUE:
┌─────────────────────────────────────────┐
│ Step 1: Create state │
│ const [value, setValue] = useState("");│
│ │
│ Step 2: Connect state to input │
│ <input value={value} /> │
│ │
│ Step 3: Update state on change │
│ onChange={e => setValue(e.target.value)}│
│ │
│ e.target = the input element │
│ e.target.value = what's typed │
└─────────────────────────────────────────┘
e.target BREAKDOWN:
┌─────────────────────────────────────────┐
│ e (event object) │
│ └── target (the element) │
│ └── value (the text inside) │
│ │
│ e.target.value = "what user typed" │
└─────────────────────────────────────────┘
*/
// ==========================================
// EXAMPLE 1: Basic Controlled Input (3 Steps)
// ==========================================
import { useState } from 'react';
function ControlledInput() {
// Step 1: Create state
const [description, setDescription] = useState("");
return (
<div>
{/*
Step 2: value={state}
Step 3: onChange updates state
*/}
<input
type="text"
placeholder="Type something..."
value={description} // ← Step 2
onChange={e => setDescription(e.target.value)} // ← Step 3
/>
<p>You typed: {description}</p>
</div>
);
}
// Visual Flow:
// Initial: description = ""
// input shows: "" (empty)
// <p>You typed: </p>
//
// User types "Hi":
// onChange → e.target.value = "H"
// setDescription("H")
// React re-renders
// description = "H"
// input shows: "H"
// <p>You typed: H</p>
//
// User types "i":
// onChange → e.target.value = "Hi"
// setDescription("Hi")
// React re-renders
// description = "Hi"
// input shows: "Hi"
// <p>You typed: Hi</p>
// ==========================================
// EXAMPLE 2: Controlled Select (Dropdown)
// ==========================================
function ControlledSelect() {
// Step 1: Create state (number, starts at 1)
const [quantity, setQuantity] = useState(1);
return (
<div>
<select
value={quantity} // ← Step 2: Connect state
onChange={e => setQuantity(Number(e.target.value))} // ← Step 3: Update (convert to number!)
>
{Array.from({ length: 20 }, (_, i) => i + 1).map(num => (
<option value={num} key={num}>
{num}
</option>
))}
</select>
<p>Selected quantity: {quantity} (type: {typeof quantity})</p>
</div>
);
}
// ⚠️ IMPORTANT: e.target.value is ALWAYS a string!
// Even for <select> with numeric options!
//
// Without Number():
// quantity = "5" (string!) ❌
//
// With Number():
// quantity = 5 (number!) ✓
//
// Other ways to convert:
// Number(e.target.value) ← Most readable
// +e.target.value ← Quick trick
// parseInt(e.target.value) ← Also works
// ==========================================
// EXAMPLE 3: Complete Controlled Form
// ==========================================
function ControlledForm() {
// Step 1: Create states for EACH input
const [description, setDescription] = useState("");
const [quantity, setQuantity] = useState(1);
function handleSubmit(e) {
e.preventDefault();
// Easy to get values! They're in state!
const newItem = {
id: Date.now(),
description, // Same as description: description
quantity, // Same as quantity: quantity
packed: false
};
console.log(newItem);
// { id: 123456789, description: "Socks", quantity: 5, packed: false }
}
return (
<form className="add-form" onSubmit={handleSubmit}>
{/* Controlled Select */}
<select
value={quantity}
onChange={e => setQuantity(Number(e.target.value))}
>
{Array.from({ length: 20 }, (_, i) => i + 1).map(num => (
<option value={num} key={num}>{num}</option>
))}
</select>
{/* Controlled Input */}
<input
type="text"
placeholder="Item..."
value={description}
onChange={e => setDescription(e.target.value)}
/>
<button>Add</button>
</form>
);
}
// ==========================================
// EXAMPLE 4: Form Validation & Reset
// ==========================================
function FormWithValidation() {
const [description, setDescription] = useState("");
const [quantity, setQuantity] = useState(1);
function handleSubmit(e) {
e.preventDefault();
// VALIDATION: Don't submit empty items!
if (!description) return; // Guard clause
const newItem = {
id: Date.now(),
description,
quantity,
packed: false
};
console.log(newItem);
// RESET: Clear form after submission!
setDescription(""); // Clear input
setQuantity(1); // Reset to default
}
return (
<form className="add-form" onSubmit={handleSubmit}>
<select
value={quantity}
onChange={e => setQuantity(Number(e.target.value))}
>
{Array.from({ length: 20 }, (_, i) => i + 1).map(num => (
<option value={num} key={num}>{num}</option>
))}
</select>
<input
type="text"
placeholder="Item..."
value={description}
onChange={e => setDescription(e.target.value)}
/>
<button>Add</button>
</form>
);
}
// Visual Flow of Submit:
// 1. User types "Socks", selects quantity 5
// 2. User clicks "Add" (or presses Enter)
// 3. handleSubmit runs
// 4. e.preventDefault() stops reload
// 5. Check: description = "Socks" (not empty) ✓
// 6. Create newItem object
// 7. Log to console
// 8. setDescription("") → clears input
// 9. setQuantity(1) → resets dropdown
// 10. React re-renders → form is empty again!
// ==========================================
// EXAMPLE 5: What Happens WITHOUT onChange
// ==========================================
function BrokenControlledInput() {
const [description, setDescription] = useState("");
return (
<div>
{/*
❌ WRONG: value without onChange!
React sets value to "" (empty string)
But there's no way to update it!
User CANNOT type anything!
*/}
<input
type="text"
value={description}
// Missing onChange! 😱
/>
<p>This input is FROZEN! You can't type!</p>
</div>
);
}
// Why it's frozen:
// 1. Initial: description = ""
// 2. React renders input with value=""
// 3. User tries to type "A"
// 4. DOM wants to show "A"
// 5. But React says "value must be description (which is '')"
// 6. React overrides DOM → still shows ""
// 7. User can't type anything!
// Fix: Add onChange!
// onChange={e => setDescription(e.target.value)}
// ==========================================
// EXAMPLE 6: The Complete "Far Away" Form
// ==========================================
const initialItems = [
{ id: 1, description: "Passport", quantity: 1, packed: false },
{ id: 2, description: "Socks", quantity: 6, packed: false },
];
function Form({ onAddItem }) {
const [description, setDescription] = useState("");
const [quantity, setQuantity] = useState(1);
function handleSubmit(e) {
e.preventDefault();
if (!description) return;
const newItem = {
id: Date.now(),
description,
quantity,
packed: false
};
onAddItem(newItem); // Send to parent!
// Reset
setDescription("");
setQuantity(1);
}
return (
<form className="add-form" onSubmit={handleSubmit}>
<select
value={quantity}
onChange={e => setQuantity(Number(e.target.value))}
>
{Array.from({ length: 20 }, (_, i) => i + 1).map(num => (
<option value={num} key={num}>{num}</option>
))}
</select>
<input
type="text"
placeholder="Item..."
value={description}
onChange={e => setDescription(e.target.value)}
/>
<button>Add</button>
</form>
);
}
🚀 Interactive React Usage Examples
Complete React File: ControlledComponentsMasterClass.jsx
import React, { useState } from 'react';
// ==========================================
// INTERACTIVE CONTROLLED COMPONENTS DEMO
// ==========================================
function ControlledComponentsMasterClass() {
const [activeDemo, setActiveDemo] = useState('steps');
const demos = {
steps: { title: '3 Steps', component: <ThreeStepsDemo /> },
input: { title: 'Text Input', component: <TextInputDemo /> },
select: { title: 'Select/Dropdown', component: <SelectDemo /> },
complete: { title: 'Complete Form', component: <CompleteFormDemo /> },
reset: { title: 'Validation & Reset', component: <ValidationResetDemo /> },
broken: { title: 'Broken (No onChange)', component: <BrokenDemo /> }
};
return (
<div style={{ maxWidth: '900px', margin: '0 auto', padding: '20px', fontFamily: 'Arial, sans-serif' }}>
<header style={{ background: '#e76f51', color: 'white', padding: '30px', borderRadius: '10px', marginBottom: '30px' }}>
<h1 style={{ margin: 0 }}>🎮 Controlled Components</h1>
<p style={{ margin: '10px 0 0 0', opacity: 0.9 }}>React Owns the Form State</p>
</header>
<nav style={{ display: 'flex', gap: '10px', marginBottom: '30px', flexWrap: 'wrap' }}>
{Object.entries(demos).map(([key, { title }]) => (
<button
key={key}
onClick={() => setActiveDemo(key)}
style={{
padding: '12px 24px',
fontSize: '16px',
border: 'none',
borderRadius: '8px',
cursor: 'pointer',
backgroundColor: activeDemo === key ? '#264653' : '#e0e0e0',
color: activeDemo === key ? 'white' : '#333',
fontWeight: activeDemo === key ? 'bold' : 'normal'
}}
>
{title}
</button>
))}
</nav>
<main style={{ background: '#f8f9fa', padding: '30px', borderRadius: '10px', minHeight: '400px' }}>
{demos[activeDemo].component}
</main>
</div>
);
}
// ==========================================
// DEMO 1: The 3 Steps
// ==========================================
function ThreeStepsDemo() {
const [step, setStep] = useState(1);
const steps = [
{
num: 1,
title: 'Create State',
code: 'const [text, setText] = useState("");',
visual: (
<div style={{ padding: '20px', background: '#e3f2fd', borderRadius: '8px', textAlign: 'center' }}>
<div style={{ fontSize: '48px' }}>📦</div>
<div style={{ fontSize: '20px', fontWeight: 'bold', color: '#264653' }}>State Box</div>
<div style={{ fontSize: '24px', color: '#7950f2', fontFamily: 'monospace' }}>""</div>
<div style={{ fontSize: '14px', color: '#666' }}>Empty string</div>
</div>
),
desc: 'Create a box in React memory to hold the input value'
},
{
num: 2,
title: 'Connect to Input',
code: '<input value={text} />',
visual: (
<div style={{ padding: '20px', background: '#e8f5e9', borderRadius: '8px', textAlign: 'center' }}>
<div style={{ fontSize: '48px' }}>🔗</div>
<div style={{ fontSize: '20px', fontWeight: 'bold', color: '#264653' }}>Connection</div>
<div style={{ fontSize: '14px', color: '#666' }}>
Input displays whatever is in the state box
</div>
<div style={{ marginTop: '10px', padding: '10px', background: 'white', borderRadius: '4px', fontFamily: 'monospace' }}>
value = ""
</div>
</div>
),
desc: 'Tell the input: "Your value comes from React state"'
},
{
num: 3,
title: 'Update on Change',
code: 'onChange={e => setText(e.target.value)}',
visual: (
<div style={{ padding: '20px', background: '#fff3e0', borderRadius: '8px', textAlign: 'center' }}>
<div style={{ fontSize: '48px' }}>🔄</div>
<div style={{ fontSize: '20px', fontWeight: 'bold', color: '#264653' }}>Update Loop</div>
<div style={{ fontSize: '14px', color: '#666' }}>
When user types, update state, which updates input
</div>
<div style={{ marginTop: '10px', fontSize: '12px', color: '#999' }}>
Type → onChange → setState → Re-render → New value
</div>
</div>
),
desc: 'When user types, update the state box, which updates the input'
}
];
const current = steps[step - 1];
return (
<div>
<h2>The 3-Step Technique 🎯</h2>
<div style={{ display: 'flex', gap: '10px', marginBottom: '20px', justifyContent: 'center' }}>
{steps.map(s => (
<button
key={s.num}
onClick={() => setStep(s.num)}
style={{
width: '60px',
height: '60px',
borderRadius: '50%',
border: 'none',
cursor: 'pointer',
background: step >= s.num ? '#7950f2' : '#e0e0e0',
color: step >= s.num ? 'white' : '#333',
fontSize: '24px',
fontWeight: 'bold'
}}
>
{s.num}
</button>
))}
</div>
<div style={{ padding: '30px', background: 'white', borderRadius: '10px', border: '2px solid #264653' }}>
<h3 style={{ color: '#264653', marginTop: 0 }}>Step {current.num}: {current.title}</h3>
<div style={{ display: 'grid', gap: '20px', gridTemplateColumns: '1fr 1fr', marginBottom: '20px' }}>
<div>
<pre style={{
background: '#1e1e1e',
color: '#d4d4d4',
padding: '20px',
borderRadius: '8px',
fontSize: '16px'
}}>
{current.code}
</pre>
<p style={{ fontSize: '18px', color: '#555' }}>{current.desc}</p>
</div>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
{current.visual}
</div>
</div>
</div>
<div style={{ marginTop: '20px', padding: '15px', background: '#e8f5e9', borderRadius: '8px' }}>
<h4>🧠 Memory Trick:</h4>
<p>"React holds the remote control (state), the input is the TV (display), and onChange is the button that changes the channel!"</p>
</div>
</div>
);
}
// ==========================================
// DEMO 2: Text Input
// ==========================================
function TextInputDemo() {
const [text, setText] = useState("");
const [showExplanation, setShowExplanation] = useState(false);
return (
<div>
<h2>Controlled Text Input ⌨️</h2>
<div style={{ marginBottom: '20px', padding: '15px', background: '#e3f2fd', borderRadius: '8px' }}>
<h4>Watch how React state and input stay in sync!</h4>
</div>
<div style={{
padding: '40px',
background: 'white',
borderRadius: '10px',
textAlign: 'center',
boxShadow: '0 2px 10px rgba(0,0,0,0.1)'
}}>
<input
type="text"
placeholder="Type something..."
value={text}
onChange={e => setText(e.target.value)}
style={{
padding: '15px',
fontSize: '18px',
width: '300px',
border: '2px solid #7950f2',
borderRadius: '8px'
}}
/>
<div style={{ marginTop: '30px', display: 'flex', gap: '20px', justifyContent: 'center' }}>
<div style={{ padding: '20px', background: '#e3f2fd', borderRadius: '8px', minWidth: '150px' }}>
<div style={{ fontSize: '12px', color: '#666' }}>State Value</div>
<div style={{ fontSize: '24px', fontWeight: 'bold', color: '#7950f2', fontFamily: 'monospace' }}>
"{text}"
</div>
</div>
<div style={{ padding: '20px', background: '#fff3e0', borderRadius: '8px', minWidth: '150px' }}>
<div style={{ fontSize: '12px', color: '#666' }}>Length</div>
<div style={{ fontSize: '24px', fontWeight: 'bold', color: '#e76f51' }}>
{text.length}
</div>
</div>
</div>
</div>
<div style={{ marginTop: '20px', textAlign: 'center' }}>
<button
onClick={() => setShowExplanation(!showExplanation)}
style={{ padding: '10px 20px', background: '#264653', color: 'white', border: 'none', borderRadius: '5px' }}
>
{showExplanation ? 'Hide' : 'Show'} How It Works
</button>
</div>
{showExplanation && (
<div style={{ marginTop: '20px', padding: '20px', background: '#fff3e0', borderRadius: '8px' }}>
<h4>🔍 Step by Step:</h4>
<ol style={{ lineHeight: '2' }}>
<li>User types letter "A"</li>
<li><code>onChange</code> fires with <code>e.target.value = "A"</code></li>
<li><code>setText("A")</code> updates state</li>
<li>React re-renders component</li>
<li><code>value={text}</code> now equals "A"</li>
<li>Input displays "A"</li>
<li>State and input are in sync! ✨</li>
</ol>
</div>
)}
</div>
);
}
// ==========================================
// DEMO 3: Select/Dropdown
// ==========================================
function SelectDemo() {
const [quantity, setQuantity] = useState(1);
const [showType, setShowType] = useState(false);
return (
<div>
<h2>Controlled Select 📋</h2>
<div style={{ marginBottom: '20px', padding: '15px', background: '#e3f2fd', borderRadius: '8px' }}>
<h4>Remember: e.target.value is ALWAYS a string!</h4>
</div>
<div style={{
padding: '40px',
background: 'white',
borderRadius: '10px',
textAlign: 'center',
boxShadow: '0 2px 10px rgba(0,0,0,0.1)'
}}>
<div style={{ marginBottom: '20px' }}>
<select
value={quantity}
onChange={e => {
const value = e.target.value;
const numberValue = Number(value);
setQuantity(numberValue);
}}
style={{
padding: '15px',
fontSize: '18px',
width: '200px',
border: '2px solid #2a9d8f',
borderRadius: '8px'
}}
>
{Array.from({ length: 20 }, (_, i) => i + 1).map(num => (
<option value={num} key={num}>{num}</option>
))}
</select>
</div>
<div style={{ display: 'flex', gap: '20px', justifyContent: 'center' }}>
<div style={{ padding: '20px', background: '#e8f5e9', borderRadius: '8px', minWidth: '150px' }}>
<div style={{ fontSize: '12px', color: '#666' }}>Value</div>
<div style={{ fontSize: '24px', fontWeight: 'bold', color: '#2a9d8f' }}>
{quantity}
</div>
</div>
<div
style={{
padding: '20px',
background: showType ? '#ffebee' : '#e8f5e9',
borderRadius: '8px',
minWidth: '150px',
cursor: 'pointer'
}}
onClick={() => setShowType(!showType)}
>
<div style={{ fontSize: '12px', color: '#666' }}>Type</div>
<div style={{ fontSize: '24px', fontWeight: 'bold', color: showType ? '#c62828' : '#2e7d32' }}>
{showType ? 'string!' : 'number ✓'}
</div>
<div style={{ fontSize: '12px', color: '#999' }}>Click to reveal!</div>
</div>
</div>
<div style={{ marginTop: '20px', padding: '15px', background: '#fff3e0', borderRadius: '8px', textAlign: 'left' }}>
<h4>⚠️ The Number Conversion Trap:</h4>
<div style={{ fontFamily: 'monospace', fontSize: '14px', lineHeight: '2' }}>
<span style={{ color: '#c62828' }}>❌ Without Number():</span><br/>
e.target.value → "5" (string)<br/>
setQuantity("5") → state = "5"<br/>
<br/>
<span style={{ color: '#2e7d32' }}>✅ With Number():</span><br/>
e.target.value → "5" (string)<br/>
Number("5") → 5 (number)<br/>
setQuantity(5) → state = 5 ✓
</div>
</div>
</div>
</div>
);
}
// ==========================================
// DEMO 4: Complete Form
// ==========================================
function CompleteFormDemo() {
const [description, setDescription] = useState("");
const [quantity, setQuantity] = useState(1);
const [items, setItems] = useState([]);
function handleSubmit(e) {
e.preventDefault();
if (!description) return;
const newItem = {
id: Date.now(),
description,
quantity,
packed: false
};
setItems(prev => [...prev, newItem]);
setDescription("");
setQuantity(1);
}
return (
<div>
<h2>Complete Controlled Form 📝</h2>
<div style={{ marginBottom: '20px', padding: '15px', background: '#e3f2fd', borderRadius: '8px' }}>
<h4>All 3 steps applied to both inputs!</h4>
</div>
<div style={{
maxWidth: '500px',
margin: '0 auto',
background: 'white',
borderRadius: '10px',
overflow: 'hidden',
boxShadow: '0 4px 20px rgba(0,0,0,0.2)'
}}>
{/* Form */}
<form
onSubmit={handleSubmit}
style={{
display: 'flex',
gap: '10px',
padding: '20px',
background: '#e76f51',
alignItems: 'center'
}}
>
<select
value={quantity}
onChange={e => setQuantity(Number(e.target.value))}
style={{ padding: '10px', borderRadius: '5px', border: 'none', fontSize: '16px' }}
>
{Array.from({ length: 20 }, (_, i) => i + 1).map(num => (
<option value={num} key={num}>{num}</option>
))}
</select>
<input
type="text"
placeholder="Item..."
value={description}
onChange={e => setDescription(e.target.value)}
style={{ flex: 1, padding: '10px', borderRadius: '5px', border: 'none', fontSize: '16px' }}
/>
<button style={{ padding: '10px 20px', background: '#264653', color: 'white', border: 'none', borderRadius: '5px', cursor: 'pointer' }}>
Add
</button>
</form>
{/* List */}
<div style={{ padding: '20px', background: '#f4a261', minHeight: '150px' }}>
{items.length === 0 ? (
<p style={{ textAlign: 'center', color: 'white' }}>Add some items!</p>
) : (
<ul style={{ listStyle: 'none', padding: 0, margin: 0 }}>
{items.map(item => (
<li key={item.id} style={{
padding: '10px',
marginBottom: '5px',
background: 'white',
borderRadius: '5px',
display: 'flex',
gap: '10px'
}}>
<span style={{ background: '#7950f2', color: 'white', padding: '2px 8px', borderRadius: '50%', fontSize: '14px' }}>
{item.quantity}
</span>
<span>{item.description}</span>
</li>
))}
</ul>
)}
</div>
</div>
<div style={{ marginTop: '20px', padding: '15px', background: '#e8f5e9', borderRadius: '8px' }}>
<h4>✅ What This Form Does:</h4>
<ol>
<li>React owns both <code>description</code> and <code>quantity</code> state</li>
<li>Inputs are controlled (value + onChange)</li>
<li>Submit creates new item from state</li>
<li>Form resets after submission</li>
<li>New item appears in list instantly!</li>
</ol>
</div>
</div>
);
}
// ==========================================
// DEMO 5: Validation & Reset
// ==========================================
function ValidationResetDemo() {
const [description, setDescription] = useState("");
const [quantity, setQuantity] = useState(1);
const [message, setMessage] = useState("");
function handleSubmit(e) {
e.preventDefault();
// VALIDATION
if (!description) {
setMessage("❌ Please enter an item description!");
return;
}
setMessage(`✅ Added: ${quantity} x ${description}`);
// RESET
setDescription("");
setQuantity(1);
}
return (
<div>
<h2>Validation & Reset 🛡️</h2>
<div style={{ marginBottom: '20px', padding: '15px', background: '#e3f2fd', borderRadius: '8px' }}>
<h4>Guard clause prevents empty submissions. Reset clears the form!</h4>
</div>
<div style={{
padding: '40px',
background: 'white',
borderRadius: '10px',
textAlign: 'center',
boxShadow: '0 2px 10px rgba(0,0,0,0.1)'
}}>
<form
onSubmit={handleSubmit}
style={{ display: 'flex', gap: '10px', justifyContent: 'center', marginBottom: '20px' }}
>
<select
value={quantity}
onChange={e => setQuantity(Number(e.target.value))}
style={{ padding: '10px', borderRadius: '5px', border: '2px solid #7950f2' }}
>
{Array.from({ length: 10 }, (_, i) => i + 1).map(num => (
<option value={num} key={num}>{num}</option>
))}
</select>
<input
type="text"
placeholder="Item..."
value={description}
onChange={e => setDescription(e.target.value)}
style={{ padding: '10px', borderRadius: '5px', border: '2px solid #7950f2', width: '200px' }}
/>
<button style={{ padding: '10px 20px', background: '#7950f2', color: 'white', border: 'none', borderRadius: '5px', cursor: 'pointer' }}>
Add
</button>
</form>
{message && (
<div style={{
padding: '15px',
background: message.startsWith('✅') ? '#e8f5e9' : '#ffebee',
color: message.startsWith('✅') ? '#2e7d32' : '#c62828',
borderRadius: '8px',
fontWeight: 'bold'
}}>
{message}
</div>
)}
<div style={{ marginTop: '20px', display: 'flex', gap: '20px', justifyContent: 'center' }}>
<div style={{ padding: '15px', background: '#f5f5f5', borderRadius: '8px' }}>
<div style={{ fontSize: '12px', color: '#666' }}>description state</div>
<div style={{ fontSize: '20px', fontWeight: 'bold', color: '#7950f2' }}>"{description}"</div>
</div>
<div style={{ padding: '15px', background: '#f5f5f5', borderRadius: '8px' }}>
<div style={{ fontSize: '12px', color: '#666' }}>quantity state</div>
<div style={{ fontSize: '20px', fontWeight: 'bold', color: '#7950f2' }}>{quantity}</div>
</div>
</div>
</div>
<div style={{ marginTop: '20px', padding: '15px', background: '#fff3e0', borderRadius: '8px' }}>
<h4>🧠 The Guard Clause:</h4>
<pre style={{ background: '#1e1e1e', color: '#d4d4d4', padding: '15px', borderRadius: '4px' }}>
{`if (!description) return;
// "If description is empty, STOP here!
// Don't create item, don't reset form."`}
</pre>
</div>
</div>
);
}
// ==========================================
// DEMO 6: Broken (No onChange)
// ==========================================
function BrokenDemo() {
const [description, setDescription] = useState("");
return (
<div>
<h2>Broken Input (Missing onChange) ❌</h2>
<div style={{ marginBottom: '20px', padding: '15px', background: '#ffebee', borderRadius: '8px' }}>
<h4>This input is FROZEN! Try typing!</h4>
</div>
<div style={{
padding: '40px',
background: 'white',
borderRadius: '10px',
textAlign: 'center',
boxShadow: '0 2px 10px rgba(0,0,0,0.1)'
}}>
<div style={{ marginBottom: '20px' }}>
<input
type="text"
placeholder="Try typing here..."
value={description}
// NO onChange! 😱
style={{
padding: '15px',
fontSize: '18px',
width: '300px',
border: '2px solid #ef5350',
borderRadius: '8px'
}}
/>
</div>
<div style={{ padding: '20px', background: '#ffebee', borderRadius: '8px' }}>
<p style={{ color: '#c62828', fontWeight: 'bold' }}>❌ You can't type anything!</p>
<p style={{ color: '#666' }}>React forces value to always be: "{description}"</p>
</div>
<div style={{ marginTop: '20px', padding: '20px', background: '#e8f5e9', borderRadius: '8px', textAlign: 'left' }}>
<h4 style={{ color: '#2e7d32' }}>✅ Fix: Add onChange!</h4>
<pre style={{ background: '#1e1e1e', color: '#d4d4d4', padding: '15px', borderRadius: '4px' }}>
{`<input
value={description}
onChange={e => setDescription(e.target.value)}
/>`}
</pre>
</div>
</div>
<div style={{ marginTop: '20px', padding: '15px', background: '#fff3e0', borderRadius: '8px' }}>
<h4>🧠 Why It's Frozen:</h4>
<ol>
<li>React sets <code>value=""</code> (empty string)</li>
<li>User types "A"</li>
<li>DOM wants to show "A"</li>
<li>But React says "No! value must be state!"</li>
<li>State is still "", so input shows ""</li>
<li>User can NEVER type anything! 😱</li>
</ol>
</div>
</div>
);
}
export default ControlledComponentsMasterClass;
🧠 Memory Aids for Poor Logic Thinking
The "Puppet and Puppeteer" Analogy
┌─────────────────────────────────────────────────┐
│ CONTROLLED COMPONENT = PUPPET & PUPPETEER │
│ │
│ UNCONTROLLED (DOM owns state): │
│ ┌─────────────────────────────────────────┐ │
│ │ 🎭 PUPPET (Input) controls itself! │ │
│ │ │ │
│ │ Puppet moves its own arms │ │
│ │ Puppeteer (React) watches confused │ │
│ │ "What is the puppet doing?" │ │
│ │ │ │
│ │ React: "What's in the input?" │ │
│ │ DOM: "I don't know, check yourself!" │ │
│ │ React: 😫 "I have to query the DOM!" │ │
│ └─────────────────────────────────────────┘ │
│ │
│ CONTROLLED (React owns state): │
│ ┌─────────────────────────────────────────┐ │
│ │ 🎭 PUPPETEER (React) controls puppet! │ │
│ │ │ │
│ │ Puppeteer pulls strings │ │
│ │ Puppet moves exactly as told │ │
│ │ Puppeteer knows EVERY move │ │
│ │ │ │
│ │ React: "What's in the input?" │ │
│ │ State: "It's 'Socks'!" │ │
│ │ React: 😊 "I already know!" │ │
│ └─────────────────────────────────────────┘ │
│ │
│ The Strings: │
│ • value={state} ← Puppeteer sets position │
│ • onChange={...} ← Puppeteer watches moves │
│ • setState(...) ← Puppeteer pulls string │
└─────────────────────────────────────────────────┘
The "Echo Chamber" Analogy
┌─────────────────────────────────────────────────┐
│ CONTROLLED INPUT = ECHO CHAMBER │
│ │
│ Imagine you're in a room with perfect echo: │
│ │
│ You say: "Hello" │
│ Echo repeats: "Hello" │
│ You say: "Hi" │
│ Echo repeats: "Hi" │
│ │
│ The echo ALWAYS matches what you say! │
│ │
│ In React: │
│ ┌─────────────────────────────────────────┐ │
│ │ USER TYPES: "S" │ │
│ │ ↓ │ │
│ │ onChange fires │ │
│ │ ↓ │ │
│ │ setDescription("S") │ │
│ │ ↓ │ │
│ │ State updates to "S" │ │
│ │ ↓ │ │
│ │ React re-renders │ │
│ │ ↓ │ │
│ │ value={description} → value="S" │ │
│ │ ↓ │ │
│ │ INPUT SHOWS: "S" ✓ │ │
│ │ (Echo matches what user typed!) │ │
│ └─────────────────────────────────────────┘ │
│ │
│ WITHOUT onChange (Broken): │
│ ┌─────────────────────────────────────────┐ │
│ │ USER TYPES: "S" │ │
│ │ ↓ │ │
│ │ (No onChange to hear it!) │ │
│ │ ↓ │ │
│ │ State stays "" │ │
│ │ ↓ │ │
│ │ value={description} → value="" │ │
│ │ ↓ │ │
│ │ INPUT SHOWS: "" ❌ │ │
│ │ (Echo ignores user! Frozen!) │ │
│ └─────────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘
The "Thermostat" Analogy for Number Conversion
┌─────────────────────────────────────────────────┐
│ e.target.value = THERMOSTAT WITH STICKY NOTE │
│ │
│ The thermostat always shows a STRING: │
│ │
│ ┌─────────────────────────────────────────┐ │
│ │ 🌡️ THERMOSTAT DISPLAY │ │
│ │ │ │
│ │ Shows: "72" ← This is TEXT! │ │
│ │ Not: 72 ← Not a real number! │ │
│ │ │ │
│ │ e.target.value always returns TEXT │ │
│ │ Even for <select> with numbers! │ │
│ └─────────────────────────────────────────┘ │
│ │
│ You need to CONVERT it: │
│ ┌─────────────────────────────────────────┐ │
│ │ "72" (string) → Number("72") → 72 │ │
│ │ │ │
│ │ Like peeling off the sticky note: │ │
│ │ "72" → 72 │ │
│ │ Text → Real Number │ │
│ └─────────────────────────────────────────┘ │
│ │
│ Why it matters: │
│ "5" + "5" = "55" (string concatenation!) │
│ 5 + 5 = 10 (number addition!) │
│ │
│ If you don't convert: │
│ quantity = "5" │
│ quantity + 1 = "51" (Wrong! String!) │
│ │
│ If you convert: │
│ quantity = 5 │
│ quantity + 1 = 6 (Right! Number!) │
└─────────────────────────────────────────────────┘
The "Guard Dog" Analogy for Validation
┌─────────────────────────────────────────────────┐
│ VALIDATION = GUARD DOG AT THE DOOR │
│ │
│ function handleSubmit(e) { │
│ e.preventDefault(); │
│ │
│ if (!description) return; ← GUARD DOG! │
│ // "If nothing to carry, go away!" │
│ │
│ // Only passes if description exists! │
│ } │
│ │
│ ┌─────────────────────────────────────────┐ │
│ │ 🐕 GUARD DOG SITUATION │ │
│ │ │ │
│ │ Person arrives with empty hands: │ │
│ │ Dog: "STOP! No empty submissions!" │ │
│ │ → return; (kick them out!) │ │
│ │ │ │
│ │ Person arrives with "Socks": │ │
│ │ Dog: "OK, you may pass!" │ │
│ │ → Continue to create item... │ │
│ └─────────────────────────────────────────┘ │
│ │
│ The ! (not) operator: │
│ !"" → true (empty string is "falsy") │
│ !"Socks" → false (non-empty is "truthy") │
│ │
│ So: if (!description) means │
│ "If description is empty/falsy..." │
└─────────────────────────────────────────────────┘
🎓 Practice Exercises
Exercise 1: Basic Controlled Input
Create a controlled input that shows what you type:
function EchoInput() {
// TODO: Create state for text
// TODO: Connect value to state
// TODO: Update state on change
// TODO: Show typed text below
return (
<div>
<input placeholder="Type here..." />
<p>You typed: {/* show text here */}</p>
</div>
);
}
Solution:
import { useState } from 'react';
function EchoInput() {
const [text, setText] = useState("");
return (
<div style={{ padding: '20px', textAlign: 'center' }}>
<input
type="text"
placeholder="Type here..."
value={text}
onChange={e => setText(e.target.value)}
style={{ padding: '10px', fontSize: '16px', width: '250px' }}
/>
<p style={{ marginTop: '20px', fontSize: '18px' }}>
You typed: <strong>"{text}"</strong>
</p>
</div>
);
}
Exercise 2: Controlled Select with Number Conversion
Create a dropdown for selecting quantity (1-10) and show the type:
function QuantitySelect() {
// TODO: Create state for quantity (number, default 1)
// TODO: Connect select value to state
// TODO: Convert e.target.value to number
// TODO: Show selected value and type
return (
<div>
<select>{/* options */}</select>
<p>Selected: {/* value */} (type: {/* typeof */})</p>
</div>
);
}
Solution:
import { useState } from 'react';
function QuantitySelect() {
const [quantity, setQuantity] = useState(1);
return (
<div style={{ padding: '20px', textAlign: 'center' }}>
<select
value={quantity}
onChange={e => setQuantity(Number(e.target.value))}
style={{ padding: '10px', fontSize: '16px' }}
>
{Array.from({ length: 10 }, (_, i) => i + 1).map(num => (
<option value={num} key={num}>{num}</option>
))}
</select>
<p style={{ marginTop: '20px', fontSize: '18px' }}>
Selected: <strong>{quantity}</strong> (type: {typeof quantity})
</p>
</div>
);
}
Exercise 3: Complete Form with Validation
Build a form that only submits if both fields are filled:
function ValidatedForm() {
// TODO: State for name and email
// TODO: Controlled inputs for both
// TODO: Validate on submit (both required)
// TODO: Show error or success message
// TODO: Reset form after success
return (
<form>
{/* Your inputs here */}
</form>
);
}
Solution:
import { useState } from 'react';
function ValidatedForm() {
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [message, setMessage] = useState("");
function handleSubmit(e) {
e.preventDefault();
// Validation
if (!name || !email) {
setMessage("❌ Please fill in all fields!");
return;
}
setMessage(`✅ Welcome, ${name}! We sent a confirmation to ${email}`);
// Reset
setName("");
setEmail("");
}
return (
<div style={{ padding: '20px', maxWidth: '400px', margin: '0 auto' }}>
<form onSubmit={handleSubmit} style={{ display: 'flex', flexDirection: 'column', gap: '15px' }}>
<input
type="text"
placeholder="Name"
value={name}
onChange={e => setName(e.target.value)}
style={{ padding: '12px', fontSize: '16px', borderRadius: '5px', border: '2px solid #7950f2' }}
/>
<input
type="email"
placeholder="Email"
value={email}
onChange={e => setEmail(e.target.value)}
style={{ padding: '12px', fontSize: '16px', borderRadius: '5px', border: '2px solid #7950f2' }}
/>
<button style={{ padding: '12px', fontSize: '16px', background: '#7950f2', color: 'white', border: 'none', borderRadius: '5px', cursor: 'pointer' }}>
Submit
</button>
</form>
{message && (
<div style={{
marginTop: '20px',
padding: '15px',
background: message.startsWith('✅') ? '#e8f5e9' : '#ffebee',
color: message.startsWith('✅') ? '#2e7d32' : '#c62828',
borderRadius: '8px',
textAlign: 'center',
fontWeight: 'bold'
}}>
{message}
</div>
)}
</div>
);
}
Exercise 4: Fix the Broken Input
This input doesn't work. Fix it:
function BrokenInput() {
const [value, setValue] = useState("");
return (
<div>
<input
type="text"
value={value}
// Something is missing here!
/>
<p>Value: {value}</p>
</div>
);
}
Solution:
import { useState } from 'react';
function FixedInput() {
const [value, setValue] = useState("");
return (
<div style={{ padding: '20px', textAlign: 'center' }}>
<input
type="text"
value={value}
onChange={e => setValue(e.target.value)} // ← ADD THIS!
style={{ padding: '10px', fontSize: '16px' }}
/>
<p>Value: {value}</p>
</div>
);
}
💡 Key Takeaways
| Concept | What It Means | Example |
|---|---|---|
| Controlled component | React owns the input state | value={state} onChange={...} |
| Step 1 | Create state | const [text, setText] = useState("") |
| Step 2 | Connect state to input | <input value={text} /> |
| Step 3 | Update state on change | onChange={e => setText(e.target.value)} |
| e.target | The input element | e.target.value = what's typed |
| e.target.value | Always a string! | "5" not 5 |
| Number() | Convert string to number | Number(e.target.value) |
| Guard clause | Stop if invalid | if (!description) return; |
| Reset form | Clear after submit | setDescription(""); setQuantity(1); |
| Frozen input | Missing onChange | value={state} without onChange |
The 3-Step Pattern:
function ControlledInput() {
// Step 1: Create state
const [value, setValue] = useState("");
return (
<input
// Step 2: Connect state to value
value={value}
// Step 3: Update state on change
onChange={e => setValue(e.target.value)}
/>
);
}
Golden Rules:
- Always use all 3 steps — State, value, onChange
- Never forget onChange — Or input will be frozen!
- Convert numbers —
e.target.valueis always a string - Validate before submit — Guard clause with
if (!value) return - Reset after success — Clear form for next entry
- One state per input — Each input needs its own useState
- React owns the data — Not the DOM
One Sentence Summary: > "Controlled components in React use a 3-step technique where you first create a piece of state with useState, then connect that state to the input's value prop so React controls what's displayed, and finally add an onChange handler that updates the state with e.target.value whenever the user types—remembering that e.target.value is always a string so you must convert it with Number() for numeric inputs, and always include validation with a guard clause like if (!description) return before processing the form submission, then reset the form by setting state back to initial values!"