Master JavaScript hoisting with clear examples, React use cases & real-world tips. No confusion - just deep, practical understanding.
As a frontend developer, you’ve probably encountered strange behaviors in JavaScript where variables or functions seem to exist before they were even declared. This is often attributed to something called “hoisting.”
Understanding JavaScript hoisting isn’t just about knowing a definition; it’s about mastering control over your variables, functions, and scopes. Misunderstanding hoisting can lead to bugs that are hard to trace, especially in complex applications involving event handlers, React hooks, or asynchronous callbacks.
In this article, we’ll break down hoisting in a way that not only clears up confusion but equips you to write cleaner, more predictable JavaScript code.
Whether you’re debugging a closure inside a React modal or structuring TypeScript code for better performance, hoisting is one of those core JavaScript mechanics that keeps showing up. So let’s dive deep.
Hoisting is JavaScript’s default behavior of moving declarations to the top of their scope (global or function).
This means that even if a variable or function is defined later in the code, JavaScript handles it as if it were declared at the beginning of its scope.
Hoisting only moves declarations, not initializations.
console.log(a); // undefined
var a = 10;
JavaScript hoists the declaration of a
but not the assignment. So the above code is interpreted like:
var a;
console.log(a); // undefined
a = 10;
var
is function-scoped and gets hoisted to the top of the enclosing function.
function test() {
console.log(x); // undefined
var x = 5;
console.log(x); // 5
}
Using var
can lead to unintended behavior due to its function-level scoping.
Variables declared with let
and const
are hoisted but remain in a Temporal Dead Zone (TDZ) until their declaration is evaluated.
console.log(y); // ReferenceError
let y = 10;
When dealing with state initialization or lifecycle hooks in React, mistakenly using var
instead of let
or const
can introduce side effects that are hard to debug.
function mystery() {
console.log(foo); // ???
var foo = 'bar';
return foo;
}
Try this out in a local console and explain why the result is what it is.
Function declarations are fully hoisted with their bodies.
sayHello(); // "Hello!"
function sayHello() {
console.log("Hello!");
}
Function expressions, whether anonymous or named, are treated as variable assignments.
sayHi(); // TypeError: sayHi is not a function
var sayHi = function () {
console.log("Hi!");
};
Here, sayHi
is hoisted but only as a variable with undefined
value.
If you’re using TypeScript generics to define higher-order functions, hoisting issues can become particularly confusing.
const wrap = <T>(fn: (arg: T) => void) => (arg: T) => {
console.log("Wrapping function");
fn(arg);
};
// Cannot hoist arrow function before this line
const wrapped = wrap((x: number) => console.log(x));
Variables declared in the global scope are hoisted globally, whereas those inside functions are confined to function scope.
var globalVar = 'I am global';
function localScope() {
console.log(globalVar); // "I am global"
var localVar = 'I am local';
}
Block-scoped declarations (let
and const
) stay confined to the block.
{
let blockScoped = true;
}
console.log(blockScoped); // ReferenceError
Let’s consider a React modal component that toggles visibility.
function ModalToggle() {
var isVisible = false;
function toggle() {
isVisible = !isVisible;
}
return (
<button onClick={toggle}>
Toggle Modal
</button>
);
}
isVisible
doesn’t persist across renders because it’s just a local var
. Switching to useState
would fix this, but using let
or const
could also help clarify scope.
function ModalToggle() {
const [isVisible, setIsVisible] = React.useState(false);
function toggle() {
setIsVisible(prev => !prev);
}
return (
<button onClick={toggle}>
Toggle Modal
</button>
);
}
Avoid var
in modern JavaScript. It introduces scope confusion and hoisting quirks.
Even though JavaScript allows hoisting, always declare variables and functions before using them.
Enable ESLint rules that prevent usage of undeclared variables or hoisting-prone patterns.
var
and expecting block-level scopeTry rewriting a short function using all three variable declarations (var
, let
, const
) and see how hoisting affects each one.
var
is function-scoped and hoisted with undefined
.let
and const
are hoisted but remain in TDZ.var
and prefer let
or const
.