Introduction
One of the most confusing concepts for JavaScript developers is hoisting, especially when it comes to arrow functions. You might have encountered this error:
myArrowFunction(); // β TypeError: myArrowFunction is not a function
const myArrowFunction = () => {
console.log('Hello!');
};But then you might wonder: "Why doesn't this work in arrow functions, but regular functions work fine?" And more confusingly, "Why doesn't React seem to care about hoisting?"
Let's break this down and understand what's really happening.
What is Hoisting?
Hoisting is JavaScript's behavior of moving declarations to the top of their scope before code execution. However, it's more nuanced than that.
Function Declarations are Hoisted Completely
Function declarations are fully hoisted, meaning both the declaration and the body are moved to the top:
// This works! β
console.log(greet()); // Output: "Hello, Vasanth!"
function greet() {
return 'Hello, Vasanth!';
}
// JavaScript interprets it as if you wrote:
/*
function greet() {
return 'Hello, Vasanth!';
}
console.log(greet());
*/Arrow Functions (and Function Expressions) are NOT Hoisted
Arrow functions are assigned to variables, and variables declared with const or let are not hoisted. They are in the Temporal Dead Zone (TDZ):
// This throws an error! β
console.log(greet); // ReferenceError: Cannot access 'greet' before initialization
const greet = () => {
return 'Hello, Vasanth!';
};
// var declarations ARE hoisted but with a different behavior:
console.log(myFunc); // undefined (not an error!)
var myFunc = () => {
return 'Hello!';
};
myFunc(); // Now this works β
Understanding the Difference
| Type | Hoisted? | TDZ? | Before Declaration |
|---|---|---|---|
| Function Declaration | β Yes | β No | Callable |
| Arrow Function (const) | β No | β Yes | ReferenceError |
| Arrow Function (var) | β Yes | β No | undefined |
| Function Expression (const) | β No | β Yes | ReferenceError |
Why This Matters in React
You might be thinking: "But in React, I don't get errors with arrow functions hoisting!"
This is because of how React components are typically structured:
// React Functional Component
const MyComponent = () => {
// This arrow function is defined BEFORE it's called
const handleClick = () => {
console.log('Button clicked!');
};
return (
<button onClick={handleClick}>
Click me
</button>
);
};
export default MyComponent;In this example, handleClick works perfectly fine. Why? Because:
- The component function is called after it's defined
handleClickis defined before it's used inside the component- React doesn't call the function during parsing; it calls it during render
Real-World Hoisting Issues in React
β Problem: Calling Before Declaration
const MyComponent = () => {
// β This will throw an error!
handleClick(); // ReferenceError
const handleClick = () => {
console.log('Clicked!');
};
};β Solution: Declare Before Using
const MyComponent = () => {
// β
Declare first
const handleClick = () => {
console.log('Clicked!');
};
// β
Then use
handleClick();
return <button onClick={handleClick}>Click</button>;
};Temporal Dead Zone (TDZ) Explained
The Temporal Dead Zone is the period between the start of a block and the point where a variable is declared:
// TDZ START
console.log(typeof x); // β ReferenceError (not undefined!)
// TDZ continues...
const x = 10;
// TDZ END
console.log(x); // β
10This is different from var:
console.log(typeof y); // β
"undefined" (not an error!)
var y = 10;
console.log(y); // β
10Practical Example: useEffect with Arrow Functions
One of the most common places you'll use arrow functions in React is within useEffect:
import React, { useState, useEffect } from 'react';
const UserProfile = ({ userId }) => {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
// β
Arrow function declared BEFORE useEffect
const fetchUser = async () => {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
setUser(data);
setLoading(false);
};
// β
useEffect uses the arrow function that's already defined
useEffect(() => {
fetchUser();
}, [userId]);
if (loading) return <div>Loading...</div>;
return <div>{user.name}</div>;
};Common Mistake: Arrow Functions in Dependency Array
// β PROBLEM: Function redefined on every render
const UserProfile = ({ userId }) => {
const [user, setUser] = useState(null);
useEffect(() => {
const fetchUser = async () => {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
setUser(data);
};
fetchUser();
}, []);
return <div>{user?.name}</div>;
};
// β
SOLUTION: Use useCallback
const UserProfile = ({ userId }) => {
const [user, setUser] = useState(null);
const fetchUser = useCallback(async () => {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
setUser(data);
}, [userId]);
useEffect(() => {
fetchUser();
}, [fetchUser]);
return <div>{user?.name}</div>;
};Best Practices
π― Key Takeaways
- Use const/let for arrow functions - They force you to declare before using
- Declare functions at the top of scope - This makes code more readable
- Understand TDZ - Helps you debug hoisting issues
- Be consistent - Use the same pattern throughout your project
- ESLint can help - Use rules like `no-use-before-define`
Summary Table
| Scenario | Behavior | Why? |
|---|---|---|
| Call function declaration before it's defined | β Works | Fully hoisted by JavaScript engine |
| Call arrow function (const) before declaration | β ReferenceError | Variable is in TDZ until declaration reached |
| Access arrow function variable in TDZ | β ReferenceError | Variables declared with const/let cannot be accessed in TDZ |
| Use arrow function after declaration in React | β Works | Component executes after function is fully loaded |
Conclusion
Arrow functions aren't "hoisted" the same way function declarations are because they're assigned to variables. The key is understanding that:
- Function declarations are fully hoisted and callable before their definition
- Arrow functions (with const/let) exist in the Temporal Dead Zone until their declaration is reached
- React doesn't "fix" hoisting - it just doesn't encounter the problem because functions are called after components mount
- Best practice is to declare functions before using them for code clarity
You Now Understand Arrow Function Hoisting π
From the Temporal Dead Zone to React patternsβyou've learned what confuses most developers. But understanding concepts and mastering them under interview pressure are two different things.
Our cohort has helped developers go from "I know hoisting" to "I can explain it instantly, then solve complex JavaScript problems in front of an interviewer."
Join Cohort 3 Waitlist β