Why Can't I Just Use Regular Variables? Understanding useState's Magic
I'm a backend developer learning React, and I'm confused. Why do I need useState when I can just create my own setEmail function like we do in Java? π€ Just set the variable and done! My frontend mentor showed me what happens when you try thisβand why React's useState is actually solving a critical problem I didn't even know existed. This conversation revealed the beautiful machinery working behind React's scenes.
TL;DR: The Key Problem useState Solvesβ
π΄ Regular Variables (Like Java) - Doesn't Work in Reactβ
const Login = () => {
let email = ''; // β This resets to '' EVERY render!
const setEmail = (newValue: string) => {
email = newValue; // Updates variable
// But React doesn't know to re-render!
// And even if it did, email would be '' again!
};
return <input value={email} />
};
What happens:
Render 1: email = ''
User types 't'
email = 't' β
[React re-renders somehow]
Render 2: Login() runs again
let email = ''; // RESET to empty! β
Lost your 't'!
π’ useState - The React Wayβ
const Login = () => {
const [email, setEmail] = useState('');
// React STORES this value outside the component
return <input value={email} />
};
What happens:
Render 1:
useState('') β React stores '' in memory slot #1
email = ''
User types 't':
setEmail('t') β React updates memory slot #1 to 't'
β React triggers re-render
Render 2:
Login() runs again
useState('') β React retrieves 't' from memory slot #1
email = 't' β
(preserved!)
The magic: React stores state in external memory + auto re-renders when it changes!
Problem 2: React Needs to Know When to Updateβ
// β React has no idea this changed
let email = 't';
// β
React knows immediately
setEmail('t'); // Notifies React: "Hey, something changed!"
The Magic of useStateβ
const [email, setEmail] = useState('');
What this actually does:
1. First render:
// React creates: storage[0] = ''
// Returns: ['', setEmailFunction]
2. User types 't':
setEmail('t')
β storage[0] = 't' // Update storage
β triggerReRender() // Tell React to re-render
3. Second render:
// React retrieves: storage[0] = 't'
// Returns: ['t', setEmailFunction]
// email now equals 't' β
The Naive Approach That Doesn't Workβ
Me (Backend Dev): I'm looking at this React code:
const Login = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
return (
<input
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
);
};
Coming from Java, I'm used to this:
public class Login {
private String email;
public void setEmail(String email) {
this.email = email;
}
}
My question: Why can't I just do this in React?
const Login = () => {
let email = '';
const setEmail = (value: string) => {
email = value; // Just set it!
};
return (
<input
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
);
};
Why do I need this useState thing? Can't I just create my own setter?
The Experiment: Let's Try It!β
Frontend Mentor: Great question! Let's actually try your approach and see what happens:
Your Approach (Regular Variable)β
const Login = () => {
let email = ''; // Regular variable
const setEmail = (value: string) => {
email = value;
console.log('Email set to:', email); // This will log correctly!
};
console.log('Rendering with email:', email);
return (
<input
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
);
};
What happens when you type "test":
Console:
"Rendering with email: " β Initial render
User types 't':
"Email set to: t" β Variable updated!
β But NO re-render!
User types 'e':
"Email set to: te" β Variable updated again!
β Still NO re-render!
UI shows: Empty input (still blank!) β
The variable updated, but the UI is frozen! π±
The useState Approach (Correct)β
const Login = () => {
const [email, setEmail] = useState(''); // React state
console.log('Rendering with email:', email);
return (
<input
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
);
};
What happens when you type "test":
Console:
"Rendering with email: " β Initial render
User types 't':
"Rendering with email: t" β Re-render triggered!
User types 'e':
"Rendering with email: te" β Re-render triggered!
User types 's':
"Rendering with email: tes" β Re-render triggered!
User types 't':
"Rendering with email: test" β Re-render triggered!
UI shows: Input displays "test" β
Each state change triggers a re-render, updating the UI! β¨
The Missing Piece: Re-renderingβ
Me: Wait, so the variable DID update, but the UI didn't change? Why?
Frontend Mentor: Exactly! This is the crucial insight. Let me explain:
How React Components Workβ
const Login = () => {
let email = ''; // This line runs EVERY render
return <input value={email} />;
};
What happens:
Render 1 (initial):
ββ let email = ''; // email = ''
ββ return <input value='' /> // UI shows ''
User changes input (types 't'):
ββ setEmail('t') // email = 't'
ββ ... nothing happens ... // NO re-render!
Render 2 (if somehow triggered):
ββ let email = ''; // email = '' AGAIN! (fresh variable)
ββ return <input value='' /> // UI shows '' (lost the 't'!)
The problem:
- β No re-render triggered when variable changes
- β Even if re-render happened, variable resets to
''!
The React Function Runs Again Each Renderβ
Key insight: Your component function runs completely fresh every render!
const Login = () => {
// π ALL OF THIS CODE RUNS AGAIN EACH RENDER
let email = ''; // β NEW variable each time
const setEmail = ... // β NEW function each time
return <input ... /> // β NEW JSX each time
};
It's like tearing down and rebuilding everything from scratch!
How useState Solves Thisβ
Me: So how does useState make the value persist?
Frontend Mentor: Great question! React stores the state values outside the component function. Let me show you a simplified version of how React implements useState:
Simplified React Internal Implementationβ
// This is SIMPLIFIED to show the concept
// React's internal storage (outside your component!)
let componentState: any[] = []; // Array to store all state values
let currentIndex = 0; // Track which state we're on
function useState<T>(initialValue: T): [T, (newValue: T) => void] {
// Get the index for this specific useState call
const index = currentIndex;
currentIndex++;
// If this is the first render, initialize the state
if (componentState[index] === undefined) {
componentState[index] = initialValue;
}
// Create the setter function
const setState = (newValue: T) => {
componentState[index] = newValue; // Update the stored value
reRenderComponent(); // π₯ THIS IS THE MAGIC!
};
// Return the current value and setter function
return [componentState[index], setState];
}
function reRenderComponent() {
currentIndex = 0; // Reset for next render
// Tell React to run the component function again
runComponentAgain();
}
Step-by-Step Exampleβ
First Render:
const Login = () => {
// First useState call
const [email, setEmail] = useState('');
// React internal:
// - currentIndex = 0
// - componentState[0] = ''
// - Returns: ['', setEmailFunction]
// Second useState call
const [password, setPassword] = useState('');
// React internal:
// - currentIndex = 1
// - componentState[1] = ''
// - Returns: ['', setPasswordFunction]
return <input value={email} onChange={(e) => setEmail(e.target.value)} />;
};
// After first render:
// componentState = ['', ''] β Stored OUTSIDE component
// currentIndex = 2
User Types 't':
// User types, onChange fires:
setEmail('t')
// Inside setEmail function:
componentState[0] = 't' // β
Update stored value
reRenderComponent() // β
Trigger re-render
// React runs Login() again (second render):
const Login = () => {
const [email, setEmail] = useState('');
// React internal:
// - currentIndex = 0
// - componentState[0] already exists and is 't'
// - Returns: ['t', setEmailFunction] β Returns 't', not ''!
return <input value={email} /> // β
Shows 't'
}
// After second render:
// componentState = ['t', '']
// currentIndex = 2
User Types 'e':
setEmail('te')
// Inside setEmail:
componentState[0] = 'te' // Update
reRenderComponent() // Re-render
// Third render:
const Login = () => {
const [email, setEmail] = useState('');
// Returns: ['te', setEmailFunction] β Returns 'te'!
return <input value={email} /> // β
Shows 'te'
}
The Magic: Persistent Storage + Auto Re-renderβ
Me: So useState does TWO things?
Frontend Mentor: Exactly! useState provides two critical features:
1. Persistent Storage (Outside Component)β
// β Regular variable (inside component):
const Login = () => {
let email = ''; // β Recreated fresh each render
}
// β
useState (outside component):
const Login = () => {
const [email, setEmail] = useState('');
// React stores 'email' in componentState array
// Survives between renders! β
}
2. Automatic Re-renderingβ
// β Regular setter:
const setEmail = (value: string) => {
email = value;
// No re-render triggered β
};
// β
useState setter:
const setEmail = setState; // From useState
setEmail('test');
// Internally:
// 1. Updates componentState[index]
// 2. Triggers re-render automatically β
Visual Comparisonβ
Me: Can you show me a visual comparison?
Frontend Mentor: Absolutely! Here's what happens with both approaches:
Regular Variable (Doesn't Work)β
βββββββββββββββββββββββββββββββββββββββ
β Render 1 (Initial) β
β ββ let email = '' β
β ββ const setEmail = ... β
β ββ UI: <input value="" /> β
βββββββββββββββββββββββββββββββββββββββ
β
User types 't', onChange fires
β
βββββββββββββββββββββββββββββββββββββββ
β setEmail('t') β
β ββ email = 't' β
Variable updated β
β ββ ... nothing else happens ... β
βββββββββββββββββββββββββββββββββββββββ
β
NO RE-RENDER β
β
UI still shows: <input value="" /> β Frozen!
useState (Works)β
βββββββββββββββββββββββββββββββββββββββ
β Render 1 (Initial) β
β ββ useState('') β
β β ββ componentState[0] = '' β
β ββ email = '' β
β ββ UI: <input value="" /> β
βββββββββββββββββββββββββββββββββββββββ
β
User types 't', onChange fires
β
βββββββββββββββββββββββββββββββββββββββ
β setEmail('t') β
β ββ componentState[0] = 't' β
β
β ββ reRenderComponent() β
β
βββββββββββββββββββββββββββββββββββββββ
β
RE-RENDER TRIGGERED β
β
βββββββββββββββββββββββββββββββββββββββ
β Render 2 (After state change) β
β ββ useState('') β
β β ββ Returns componentState[0] = 't'β
β ββ email = 't' β
β
β ββ UI: <input value="t" /> β
β
βββββββββββββββββββββββββββββββββββββββ
Real-World Analogy: Sticky Notes vs Filing Cabinetβ
Me: Can you give me a real-world analogy?
Frontend Mentor: Perfect! Think of it like this:
Regular Variable = Sticky Notesβ
Render 1:
ββ Write "test" on sticky note π
ββ Show sticky note on screen
ββ Component function ends
ββ Throw away sticky note ποΈ
User types 'a':
ββ Create NEW sticky note (blank!)
ββ Try to show old value...
ββ But sticky note was thrown away! β
Render 2:
ββ New blank sticky note π
ββ Lost the "test" value! β
useState = Filing Cabinetβ
Render 1:
ββ Store "test" in React's filing cabinet ποΈ
ββ Show value from cabinet on screen
ββ Component function ends
ββ Value stays safe in cabinet β
User types 'a':
ββ Update filing cabinet to "testa"
ββ Trigger re-render
Render 2:
ββ Retrieve "testa" from filing cabinet ποΈ
ββ Still have the value! β
React's filing cabinet (componentState) survives between renders!
Why Multiple useState Calls Workβ
Me: How does React know which state is which if I have multiple useState calls?
Frontend Mentor: Excellent question! This is why the order matters:
Multiple State Variablesβ
const Login = () => {
const [email, setEmail] = useState(''); // Index 0
const [password, setPassword] = useState(''); // Index 1
const [loading, setLoading] = useState(false); // Index 2
// React's internal storage:
// componentState[0] = '' (email)
// componentState[1] = '' (password)
// componentState[2] = false (loading)
};
First Renderβ
currentIndex = 0
ββ useState('') β componentState[0] = '', currentIndex++
ββ useState('') β componentState[1] = '', currentIndex++
ββ useState(false) β componentState[2] = false, currentIndex++
Result: componentState = ['', '', false]
Second Render (after setEmail('test'))β
currentIndex = 0 (reset!)
ββ useState('') β Returns componentState[0] = 'test', currentIndex++
ββ useState('') β Returns componentState[1] = '', currentIndex++
ββ useState(false) β Returns componentState[2] = false, currentIndex++
Result: email = 'test', password = '', loading = false β
React relies on the ORDER of useState calls to match values!
Why This Matters: The Rules of Hooksβ
β NEVER do this (conditional useState):
const Login = () => {
if (someCondition) {
const [email, setEmail] = useState(''); // β Order changes!
}
const [password, setPassword] = useState('');
};
// Render 1 (someCondition = true):
// componentState[0] = '' (email)
// componentState[1] = '' (password)
// Render 2 (someCondition = false):
// componentState[0] = '' (password) β WRONG! Should be email!
// Values get mixed up! π±
β
ALWAYS do this (consistent order):
const Login = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
// Order is ALWAYS the same β
};
Comparison with Backend (Java)β
Me: How is this different from Java classes?
Frontend Mentor: Great question! Let me show you the key differences:
Java (OOP with State)β
public class Login {
private String email = ""; // Instance variable (persistent)
public void setEmail(String email) {
this.email = email; // Updates instance variable
// UI updates happen separately (observers, listeners, etc.)
}
public void render() {
// Render is called manually or through framework
System.out.println("Email: " + email);
}
}
// Usage:
Login login = new Login(); // One instance
login.setEmail("test"); // Updates instance
login.render(); // Manual re-render
Key points:
- β Instance persists between method calls
- β Variables survive between renders
- β Manual re-rendering (no automatic UI updates)
- β Explicit instance management
React with useState (Functional)β
const Login = () => {
// NO class instance!
// Function runs fresh each render
const [email, setEmail] = useState(''); // React manages storage
// Render happens automatically on state change
return <input value={email} onChange={(e) => setEmail(e.target.value)} />;
};
// Usage:
<Login /> // React creates "virtual instance"
// setEmail('test') β Auto re-renders component β
Key points:
- β React manages "instance" (virtual)
- β Automatic re-rendering on state change
- β Declarative UI (no manual updates)
- β Function runs fresh each time (but React preserves state)
The Mental Model Shiftβ
Java thinking:
Instance β has state β manually trigger UI update
React thinking:
State change β automatically triggers re-render β UI updates
Why Can't You Just Force Re-render?β
Me: Why can't I just manually trigger a re-render?
Frontend Mentor: Good question! Let's see what would happen:
// Hypothetical approach:
const Login = () => {
let email = ''; // Regular variable
const setEmail = (value: string) => {
email = value;
forceReRender(); // Imagine this existed
};
return <input value={email} />;
};
Problems:
Problem 1: Value Gets Lostβ
Render 1:
ββ let email = ''
ββ UI shows ''
setEmail('test'):
ββ email = 'test' β
ββ forceReRender()
Render 2:
ββ let email = '' β NEW variable! Lost 'test'! β
ββ UI shows '' β Back to empty!
Problem 2: Manual Tracking Hellβ
const Login = () => {
let email = '';
let password = '';
let loading = false;
let error = null;
let success = false;
// You'd need to manually store ALL of these
// And retrieve them each render
// And know WHICH one changed
// π± Nightmare!
};
Problem 3: No Optimizationβ
// React can optimize because it knows WHAT changed:
setEmail('test'); // Only email changed
// React can skip re-rendering other components
// With manual re-render:
forceReRender(); // React doesn't know what changed!
// Has to check everything (slow!)
useState solves all of this automatically! β¨
The Three Magic Featuresβ
Me: So useState gives me three things?
Frontend Mentor: Exactly! Let me summarize:
1. Persistent Storageβ
const [email, setEmail] = useState('');
// React stores 'email' value between renders
// Survives function re-execution β
2. Automatic Re-renderingβ
setEmail('test');
// Triggers re-render automatically
// No manual intervention needed β
3. Automatic Value Retrievalβ
const [email, setEmail] = useState('');
// React automatically gives you the current value
// Even after multiple renders β
Common Mistakes with useStateβ
Me: What mistakes do beginners make?
Frontend Mentor: Great question! Here are the most common ones:
Mistake 1: Treating State Like Regular Variablesβ
β Wrong:
const [count, setCount] = useState(0);
count = 5; // β ERROR! Can't reassign const
count++; // β ERROR! Can't mutate directly
β
Correct:
const [count, setCount] = useState(0);
setCount(5); // Use setter
setCount(count + 1); // Use setter with new value
Mistake 2: Mutating Objects/Arraysβ
β Wrong:
const [user, setUser] = useState({ name: 'John', age: 30 });
user.age = 31; // β Mutates object directly (no re-render!)
β
Correct:
setUser({ ...user, age: 31 }); // Create new object
Mistake 3: Using State Immediately After Settingβ
β Wrong:
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(5);
console.log(count); // β Still 0! (stale value)
};
β
Correct:
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(5);
// Wait for next render to see new value
};
// Or use useEffect to react to changes:
useEffect(() => {
console.log(count); // β Logs 5 after re-render
}, [count]);
Mistake 4: Conditional useStateβ
β Wrong:
const Login = () => {
if (someCondition) {
const [email, setEmail] = useState(''); // β Breaks order!
}
};
β
Correct:
const Login = () => {
const [email, setEmail] = useState(''); // β Always called
if (someCondition) {
// Use the state conditionally, but declare it unconditionally
}
};
Key Takeawaysβ
Me: Let me make sure I understand this:
Frontend Mentor: Perfect! Here's the summary:
Why Regular Variables Don't Work:β
- No persistence: Variables reset to initial value each render
- No re-render trigger: Changing variable doesn't update UI
- Function runs fresh: Component function recreates everything each time
How useState Solves This:β
- Persistent storage: React stores values outside component function
- Automatic re-rendering: Setter function triggers re-render
- Value retrieval: React returns current value each render
The Implementation (Simplified):β
// React's internal storage (outside component)
let componentState: any[] = [];
function useState<T>(initialValue: T) {
const index = currentIndex++;
// Initialize if first render
if (componentState[index] === undefined) {
componentState[index] = initialValue;
}
// Create setter that updates + re-renders
const setState = (newValue: T) => {
componentState[index] = newValue; // β
Store value
reRenderComponent(); // β
Trigger re-render
};
return [componentState[index], setState];
}
The Mental Model:β
Regular variable:
- Sticky note π
- Thrown away each render ποΈ
- Starts fresh every time β
useState:
- Filing cabinet ποΈ
- Survives between renders β
- Auto-updates UI when changed β
The Three Magic Features:β
- Persistent storage (filing cabinet, not sticky notes)
- Automatic re-rendering (no manual triggers)
- Value retrieval (React gives you current value)
Me: This makes so much sense now! useState isn't just a setterβit's a complete state management system that handles storage, retrieval, and UI updates automatically!
Frontend Mentor: Exactly! Coming from backend/Java, you're used to managing instance state yourself. React takes a different approach: the function runs fresh each time, but React manages state externally and automatically re-renders when state changes.
This is the foundation of React's declarative programming model:
- You declare: "UI should show current email value"
- React ensures: UI updates whenever email changes
No manual DOM manipulation, no explicit re-renderingβjust pure, declarative magic! β¨
Now you understand why we can't just use regular variables in React! π
Did useState confuse you too? Share your "aha moment" in the comments! π¬
