On this page

Scope: var, let & const

4 min read

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.

varletconst
ScopeFunction (or global)BlockBlock
ReassignYesYesNo
Redeclare in same scopeYesNoNo
Use before its lineAllowed, value is undefinedReferenceErrorReferenceError

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 var is 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 let or const is 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 block
console.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 object
user = {}; // 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:

  1. var i is one variable shared by the whole function.
  2. setTimeout runs its callback later, after the loop has finished.
  3. By then i is 3, so every callback prints 3.
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 rounds
2 [ i ] 0 -> 1 -> 2 -> 3 every callback reads the same box later: 3 3 3
3
4let: a new box for every round
5 [ i = 0 ] [ i = 1 ] [ i = 2 ] each callback keeps its own box: 0 1 2

Scope 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 scope
2 (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 const for almost everything. Use let only 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 render
const [number, setNumber] = useState(0); // React keeps this between renders
function handleClick() {
count = count + 1; // changes the variable, but the screen does NOT update
setNumber(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:

  1. It is thrown away and set to 0 again on the next render.
  2. 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 seen
2var -> function scope, leaks out of blocks, avoid it
3let -> block scope, can be reassigned
4const -> 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 value
7Loop with let -> one variable per round, so each callback sees its own value
8Scope chain -> lookup goes inner -> outer -> global, never inward
9Shadowing -> an inner name hides an outer name with the same spelling
10In React -> a normal variable resets every render, so use useState for values that must persist
11Default -> const, then let, never var

Interview 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.

Next TopicHoisting & the TDZ