Scope: var, let & const
Many "why is this value wrong?" bugs in React start with scope. Before hooks, props and state, you need to know where a variable lives and who can see it.
Definition
Scope is the part of your code where a variable can be read or changed. JavaScript has three ways to declare a variable, and they differ in two things: how far the variable is visible and whether it can be reassigned.
var | let | const | |
|---|---|---|---|
| Scope | Function (or global) | Block | Block |
| Reassign | Yes | Yes | No |
| Redeclare in same scope | Yes | No | No |
| Use before its line | Allowed, value is undefined | ReferenceError | ReferenceError |
All three are hoisted (JavaScript notes them at the start of their scope). The difference is that var is set to undefined right away, while let and const sit in the Temporal Dead Zone (TDZ) until their line runs. Touching them in that zone throws an error. We cover this on the next page.
A block is a group of statements inside curly braces: the body of an if, a for, or a plain { }. The braces of an object like { name: "Asha" } are not a block.
Analogy: think of a function as a room and a block as a drawer inside it.
- A
varis a note stuck on the room's wall. Everyone in the room can read it, even if it was written while standing at a drawer. - A
letorconstis a note stuck inside one drawer. Only code inside that drawer can read it.
How It Works
var ignores blocks. It only respects functions.
function demo() {if (true) {var a = 1;let b = 2;}console.log(a); // 1, a leaked out of the if blockconsole.log(b); // ReferenceError, b only exists inside the block}
const does not mean "the value never changes". It means "the variable can never point to something else".
const user = { name: "Asha" };user.name = "Ravi"; // allowed: we changed the content of the objectuser = {}; // TypeError: we tried to re-point the variable
So const stops reassignment, but it does not freeze the object. This is why const is the default in React code. State and props must never be changed directly, so const tells the reader "this name always means the same thing".
The Classic Loop Bug
This is the most common interview question on the topic.
for (var i = 0; i < 3; i++) {setTimeout(() => console.log(i), 0);}// prints: 3 3 3
Here is what goes wrong:
var iis one variable shared by the whole function.setTimeoutruns its callback later, after the loop has finished.- By then
iis3, so every callback prints3.
for (let i = 0; i < 3; i++) {setTimeout(() => console.log(i), 0);}// prints: 0 1 2
With let, each round of the loop gets its own fresh copy of i. Each callback remembers its own copy, so each prints its own value.
1var: one box shared by all rounds2 [ i ] 0 -> 1 -> 2 -> 3 every callback reads the same box later: 3 3 33 4let: a new box for every round5 [ i = 0 ] [ i = 1 ] [ i = 2 ] each callback keeps its own box: 0 1 2Scope Chain
When JavaScript sees a name, it looks in the current scope first. If it is not there, it looks in the outer scope, then the next one, all the way up to the global scope. This lookup path is the scope chain.
const app = "Rezlo";function outer() {const page = "prep";function inner() {console.log(app, page); // finds both by walking outward}inner();}outer(); // Rezlo prep
1inner scope -> outer scope -> global scope2 (looks here (then here) (last stop)3 first)An inner scope can read outer variables. An outer scope can never read inner ones. The search only goes outward.
Shadowing
An inner variable can reuse an outer name. It shadows the outer one inside its own scope, and the outer one is not changed.
const count = 1;{const count = 2;console.log(count); // 2 (the inner count)}console.log(count); // 1 (the outer count is untouched)
Shadowing is legal but easy to misread, because the same name means two different things. Many teams turn on a linter rule (like ESLint's no-shadow) to catch it.
In React
- Use
constfor almost everything. Useletonly when you really reassign, for example a running total inside a function. - Avoid
var. It leaks out of blocks and causes bugs like the loop above. - A component function runs again on every render. Every variable inside it is created fresh each time.
That last point is why state lives in useState and not in a normal variable:
function Counter() {let count = 0; // reset to 0 on every renderconst [number, setNumber] = useState(0); // React keeps this between rendersfunction handleClick() {count = count + 1; // changes the variable, but the screen does NOT updatesetNumber(number + 1); // tells React to re-render with the new value}return <button onClick={handleClick}>{number}</button>;}
A plain let has two problems in a component:
- It is thrown away and set to
0again on the next render. - Changing it does not tell React to re-render, so the screen stays the same.
useState fixes both: React remembers the value and re-renders when you call the setter.
Summary
1Scope -> where a variable can be seen2var -> function scope, leaks out of blocks, avoid it3let -> block scope, can be reassigned4const -> block scope, cannot be re-pointed (the object inside can still change)5let / const -> hoisted, but in the TDZ until their line (using them early is an error)6Loop with var -> one shared variable, so every callback sees the last value7Loop with let -> one variable per round, so each callback sees its own value8Scope chain -> lookup goes inner -> outer -> global, never inward9Shadowing -> an inner name hides an outer name with the same spelling10In React -> a normal variable resets every render, so use useState for values that must persist11Default -> const, then let, never varInterview Answer: var vs let vs const
var is function-scoped, so it ignores blocks like if and for. let and const are block-scoped. let can be reassigned, and const cannot be re-pointed, although the object inside a const can still change.
All three are hoisted, but var starts as undefined, while let and const are in the temporal dead zone until their declaration, so using them early throws a ReferenceError.
The classic example is a setTimeout inside a for loop. With var, all callbacks share one i and print the last value. With let, each iteration has its own i, so they print 0 1 2.
I use const by default, let only when I must reassign, and avoid var. In React this matters even more, because a component runs again on every render, so values that must survive belong in useState, not in a local variable.
The next question is what happens when you use a variable before the line that declares it. That is hoisting.