🧠 You’ve Been Writing React Wrong — And You Didn’t Even Know It
The Deep Truth About How React Actually Works (Variables, useState, useRef, useCallback, useContext — Finally Explained Like a Human)
🏗️ First, The Most Important Thing Nobody Tells You
Every time state changes, React re-runs your entire component function from top to bottom.
Not part of it. All of it.
Think of your component like a recipe that gets cooked fresh every single time something changes:
State changes → React calls your function again → Everything inside re-runs → UI updates
This one fact explains every confusing thing about hooks. Keep it in your mind as you read.
🧱 Chapter 1: Normal Variables (let / var / const)
What you think happens:
const Counter = () => {
let count = 0;
return (
<button onClick={() => {
count++;
console.log(count); // shows 1, 2, 3... looks right!
}}>
Count: {count} {/* always shows 0 😢 */}
</button>
);
};What actually happens:
Every click triggers a re-render. Every re-render re-runs the function. Every time the function runs, let count = 0 starts from zero again.
Real life analogy: Imagine you’re writing your grocery list on a whiteboard. Every time someone walks into the room, they erase the whiteboard and you have to start over. That’s what React does to normal variables.
When to use normal variables: Only for values you calculate within a single render and don’t need to remember. Like formatting a date string from props. That’s it.
💾 Chapter 2: useState — React's Memory
useState is React saying: "I'll remember this value for you, even across re-renders."
const Counter = () => {
const [count, setCount] = useState(0); // React stores this for you
return (
<button onClick={() => setCount(count + 1)}>
Count: {count} ✅
</button>
);
};How it actually works under the hood:
React keeps a hidden list of state values for each component. When you call useState, React is basically doing this:
Component mounts:
→ React: “Store slot #1 = 0”User clicks, setCount(1) called:
→ React: “Update slot #1 = 1, re-render!”Re-render runs:
→ React: “Return slot #1” → returns 1 ✅
Real life analogy: useState is like a locker at the gym. React holds the key. You can put something in (setState), take something out (read the value), and it'll still be there tomorrow even if you leave and come back (re-render).
The lazy initializer trick (most devs miss this):
// ❌ This runs on EVERY render — wasteful!
const [theme, setTheme] = useState(localStorage.getItem('theme'));
// ✅ This runs only ONCE on mount — the right way
const [theme, setTheme] = useState(() => localStorage.getItem('theme'));Pass a function when the initial value is expensive to compute. React only calls it once.
When useState trips you up — Stale Closures:
const [count, setCount] = useState(0);
const handleClick = () => {
setTimeout(() => {
console.log(count); // ❌ Always prints 0!
// The function "closes over" the old count value
// It's like a photograph — frozen in time
}, 3000);
};The fix — use the functional updater form:
setCount(prev => prev + 1); // ✅ Always uses the LATEST value📌 Chapter 3: useRef — Memory Without Drama
useRef is like useState's quiet sibling. It also remembers values across renders, but it does it silently — without causing a re-render.
const stopwatch = () => {
const intervalRef = useRef(null); // remembered across renders, no re-render triggered
const [time, setTime] = useState(0); // shown in UI, triggers re-render
const start = () => {
intervalRef.current = setInterval(() => {
setTime(t => t + 1);
}, 1000);
};
const stop = () => {
clearInterval(intervalRef.current); // ✅ always has the real interval ID
};
};Real life analogy: useState is a scoreboard at a football game — everyone sees it when it changes. useRef is a coach's notebook — the coach updates it quietly and nobody else reacts to it.
The most important use case — reading fresh values in callbacks:
This is where most experienced developers get burned:
// ❌ BROKEN — repeatMode is stale inside useCallback
const [repeatMode, setRepeatMode] = useState('off');
const nextTrack = useCallback(() => {
if (repeatMode === 'one') { // This is FROZEN at 'off' forever
// ...
}
}, []); // Empty deps — repeatMode never updates hereThe fix:
// ✅ CORRECT — ref is always fresh
const [repeatMode, setRepeatMode] = useState('off'); // for UI display
const repeatModeRef = useRef('off'); // for reading in callbacks
// Keep them in sync
useEffect(() => {
repeatModeRef.current = repeatMode;
}, [repeatMode]);
const nextTrack = useCallback(() => {
if (repeatModeRef.current === 'one') { // ✅ ALWAYS current value
// works perfectly
}
}, []); // No need to add repeatMode as dependency🔁 Chapter 4: Normal Functions vs useCallback
Normal functions inside components:
const ProductCard = ({ product }) => {
// ❌ This function is RECREATED on every render
const handleAddToCart = () => {
addToCart(product.id);
};
return <Button onClick={handleAddToCart}>Add to Cart</Button>;
};Every render = new handleAddToCart function. A brand new function object. Even if the logic is identical.
Get Malith Madusankha’s stories in your inbox
Join Medium for free to get updates from this writer.
Real life analogy: Imagine hiring a new worker every morning to do the same job, then firing them at the end of the day. Same job, but you keep creating and destroying workers. That’s what React does with functions on every render without useCallback.
Why does this matter? If you pass this function to a child component, that child will also re-render — even if nothing meaningful changed — because it sees a “new” function each time.
useCallback — Memoize the function:
const ProductCard = ({ product }) => {
// ✅ Same function reference across renders (unless product changes)
const handleAddToCart = useCallback(() => {
addToCart(product.id);
}, [product.id]); // Only recreate if product.id changes
return <Button onClick={handleAddToCart}>Add to Cart</Button>;
};The dependency array — the most misunderstood part:
useCallback(() => {
doSomething(a, b);
}, [a, b]); // "Recreate this function only when a or b changes"Think of dependencies as a watch list. React watches those values and recreates the function only when they change.
// ❌ Missing dependency — stale closure bug
const greet = useCallback(() => {
console.log(`Hello, ${name}`); // name is stale!
}, []); // name not listed — never updates
// ✅ Correct
const greet = useCallback(() => {
console.log(`Hello, ${name}`);
}, [name]); // recreates when name changesWhen to actually use useCallback:
✅ Passing functions to child components that are wrapped in React.memo()
✅ Functions used in useEffect dependency arrays
✅ Functions that are called inside event listeners or timers
✅ Functions in contexts shared across many components❌ Don’t use it on every function — it has its own overhead
❌ Don’t use it if the function is only used in JSX directly
🌐 Chapter 5: useContext — The Global Walkie-Talkie
The problem it solves — Prop Drilling:
Imagine you have this component tree:
Without context, you have to pass user through every layer:
// ❌ Prop drilling — painful and fragile
<App user={user}>
<Layout user={user}>
<Sidebar user={user}>
<UserMenu user={user}>
<Avatar user={user} /> {/* finally! */}
</UserMenu>
</Sidebar>
</Layout>
</App>Change the user shape? Update 5 files. Add another prop? Another round of drilling. It’s a nightmare.
useContext to the rescue:
// 1. Create the channel
const UserContext = createContext(null);
// 2. Broadcast from the top
const App = () => {
const [user, setUser] = useState({ name: 'Alice', role: 'admin' });
return (
<UserContext.Provider value={{ user, setUser }}>
<Layout /> {/* No props needed! */}
</UserContext.Provider>
);
};
// 3. Any component tunes in directly
const Avatar = () => {
const { user } = useContext(UserContext); // ✅ Gets it directly
return <img src={user.avatar} alt={user.name} />;
};Real life analogy: Without context, passing props is like a telephone game — the message goes through 5 people before reaching its destination, and each person has to repeat it even if they don’t care about it. useContext is like a group chat — anyone can send or receive directly.
Props vs Context — When to use which:
Real world context example — an audio player:
// The "broadcast station"
const AudioContext = createContext(undefined);
export const AudioProvider = ({ children }) => {
const [currentTrack, setCurrentTrack] = useState(null);
const [isPlaying, setIsPlaying] = useState(false);
const togglePlay = useCallback(() => { /* ... */ }, []);
return (
<AudioContext.Provider value={{ currentTrack, isPlaying, togglePlay }}>
{children}
{/* The actual audio element lives here */}
<audio ref={audioRef} />
</AudioContext.Provider>
);
};
// Any component, anywhere in the app, can tune in:
const PlayerControls = () => {
const { isPlaying, togglePlay } = useContext(AudioContext);
return <button onClick={togglePlay}>{isPlaying ? '⏸' : '▶️'}</button>;
};
const TrackTitle = () => {
const { currentTrack } = useContext(AudioContext);
return <h2>{currentTrack?.title}</h2>;
};Both PlayerControls and TrackTitle can be anywhere in the component tree and they both get the same shared audio state. No props. No drilling.
🧩 Putting It All Together — The Mental Model
Here’s the complete picture in one place:
The “what should I use?” cheat sheet:
#React #JavaScript #WebDevelopment #Frontend #Programming #ReactHooks #SoftwareEngineering







