Prepared by a Senior Software Engineer
Hoisting is a JavaScript mechanism where variable and function declarations are moved to the top of their containing scope (global or local) during the compilation phase. React does not redefine hoisting rules—it inherits them from JavaScript. However, React’s component model (functions, classes, hooks) creates unique scenarios where hoisting pitfalls commonly arise.
| Declaration Type | Hoisting Behavior | Example |
|---|---|---|
var | Fully hoisted. Declared variables are initialized with undefined. | @l2: var x = 10; console.log(x); // undefined |
let/const | Not hoisted. Variables exist in a Temporal Deployment Zone (TDZ) until declared. Access before declaration throws ReferenceError. | @l2: console.log(y); let y = 10; // ReferenceError |
| Function Declaration | Fully hoisted. Can be called before declaration. | @l2: sayHello(); function sayHello() { … } |
| Function Expression | Not hoisted. Must be declared/assigned before invocation. | @l2: sayBye(); const sayBye = () => { … }; // TypeError |
🔍 React Context: These rules apply to component bodies, event handlers, and custom hooks. Misunderstanding hoisting leads to bugs in rendering logic, event callbacks, and state updates.
function MyComponent() { // ❌ Pitfall: `logVar` uses a variable declared later (hoisting issue) console.log(varA); // undefined (var is hoisted) const varA = 10; // const is NOT hoisted → ReferenceError if accessed earlier // ✅ Safe pattern: Declare all variables BEFORE using them const varB = 20; console.log(varB); // 20 return <div>{varB}</div>; }
function ListItem() { const items = [{ id: 1 }, { id: 2 }]; return ( <ul> {items.map(item => ( // ❌ BUG: `handleClick` is redefined on EVERY render ↘ // ❌ Also: `var` hoisting causes ALL handlers to reference the LAST `item` ↘ <li key={item.id}> <button onClick={() => console.log(item.id)}> {item.id} </button> </li> ))} </ul> ); }
Why This Fails:
var is hoisted inside the loop block, but not block-scoped. All onClick callbacks close over the same item (last iteration value).const (block-scoped) or extract the handler inside the map callback.useEffect, useCallback) & Hoistingfunction Component() { const [count, setCount] = useState(0); // ❌ BUG: `count` inside useEffect closes over the **initial** value (0) ↘ useEffect(() => { console.log(count); // Always logs 0 (stale closure) }, []); // ✅ Solution: Include `count` in dependencies OR use functional update useEffect(() => { console.log(count); // Updates correctly }, [count]); }
Explanation:
useEffect callbacks are ** closures ** over variables in the render scope. Hoisting doesn’t affect this, but stale closures (due to missing dependencies) are a relatedpitfall.In class components, method declarations are automatically hoisted (unlike function expressions in functional components).
class MyClass extends React.Component { // ✅ Methods are hoisted—can be called in `constructor` constructor(props) { super(props); this.handleClick(); // Works! } handleClick() { console.log("Clicked!"); } }
⚠️ Caution: If you rewrite the method as a property initializer (arrow function), it won’t be hoisted:
handleClick = () => { … }; // NOT hoisted → cannot be used in constructor
undefined? How would you fix it?function Component() { console.log(msg); // undefined var msg = "Hello"; return <div>{msg}</div>; }
Answer:
- Why?
varis fully hoisted and initialized toundefined.- Fix: Use
let/constand declare variables before use:const Component = () => { const msg = "Hello"; // Declared first console.log(msg); // "Hello" return <div>{msg}</div>; };
function TodoList() { const [todos, setTodos] = useState([]); todos.map(todo => ( <button onClick={() => setTodos([...todos, todo])}> Add {todo.text} </button> )); }
Answer:
- Bug: The
todosarray inside the map callback is stale—it captures the initial empty array due to missing dependencies. Hoisting isn’t the direct cause, but stale closures arise from improper hook usage.- Fix: Use
useEffector move logic into dependencies:function TodoList() { const [todos, setTodos] = useState([]); useEffect(() => { // Logic using LATEST todos }, [todos]); return ( <> {todos.map(todo => ( <button key={todo.id} onClick={() => setTodos(prev => [...prev, todo])} > Add {todo.text} </button> ))} </> ); }
Ideal Answer:
Hoisting matters most in these scenarios:
- Variable declarations inside loops (e.g.,
for,map) wherevarcauses unintended sharing of values across iterations.- Using
varin component logic—preferconst/letto avoidundefinedinitializations.- Class component constructors when calling methods: Only method declarations (not property initializers) are hoisted.
- Closures in hooks (
useEffect,useCallback)—hoisted variables may cause stale enclosures if dependencies are missing.
function Button() { var style = { color: "red" }; console.log(style); // undefined (var hoisted) style = { color: "blue" }; // This overrides the declaration return <button style={style}>Click</button>; }
Fix: Use const or declare before usage.
function ColorPicker() { const colors = ["red", "green", "blue"]; return ( <div> {colors.map(color => ( <button key={color} onClick={() => console.log("Selected:", color)} // ✅ Safe: `color` is block-scoped > {color} </button> ))} </div> ); }
var vs let/const, TDZ, and closure behavior.no-var, no-undef) catch hoisting misuse.Next Steps: Ready to dive deeper? Ask for a mock interview on hoisting scenarios, or request code walkthroughs for specific patterns!
Start a new session to explore different topics or increase the difficulty level.
Start New Session