Arrow Functions Hoisting in JavaScript vs React

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

TypeHoisted?TDZ?Before Declaration
Function Declarationβœ… Yes❌ NoCallable
Arrow Function (const)❌ Noβœ… YesReferenceError
Arrow Function (var)βœ… Yes❌ Noundefined
Function Expression (const)❌ Noβœ… YesReferenceError

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:

  1. The component function is called after it's defined
  2. handleClick is defined before it's used inside the component
  3. 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); // βœ… 10

This is different from var:

console.log(typeof y); // βœ… "undefined" (not an error!)
var y = 10;
console.log(y); // βœ… 10

Practical 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

ScenarioBehaviorWhy?
Call function declaration before it's definedβœ… WorksFully hoisted by JavaScript engine
Call arrow function (const) before declaration❌ ReferenceErrorVariable is in TDZ until declaration reached
Access arrow function variable in TDZ❌ ReferenceErrorVariables declared with const/let cannot be accessed in TDZ
Use arrow function after declaration in Reactβœ… WorksComponent 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:

  1. Function declarations are fully hoisted and callable before their definition
  2. Arrow functions (with const/let) exist in the Temporal Dead Zone until their declaration is reached
  3. React doesn't "fix" hoisting - it just doesn't encounter the problem because functions are called after components mount
  4. 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 β†’
← Back to Articles