Skip to main content

Why Can't I Just Use Regular Variables? Understanding useState's Magic

Β· 13 min read
Mahmut Salman
Software Developer

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:

  1. ❌ No re-render triggered when variable changes
  2. ❌ 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:​

  1. No persistence: Variables reset to initial value each render
  2. No re-render trigger: Changing variable doesn't update UI
  3. Function runs fresh: Component function recreates everything each time

How useState Solves This:​

  1. Persistent storage: React stores values outside component function
  2. Automatic re-rendering: Setter function triggers re-render
  3. 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:​

  1. Persistent storage (filing cabinet, not sticky notes)
  2. Automatic re-rendering (no manual triggers)
  3. 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! πŸ’¬