3

I've created a functional component in React like this:

import React, {useEffect} from "react";
import "./styles.css";

export default function App() {

  useEffect(() => logSomething(), [])

  const logSomething = () => console.log('whats up');
  return (<div> hello </div>)
}

This function runs without any errors, and successfully logs out "whats up" in the console.

When I try the following, however, I get a TypeError:

export default function App() {

  useEffect(logSomething(), [])

  const logSomething = () => () => console.log('whats up');
  return (<div> hello </div>)
}

Similarly, I get a TypeError when I try this:

export default function App() {

  useEffect(logSomething, [])

  const logSomething = () => console.log('whats up');
  return (<div> hello </div>)
}

Why does the first example succeed, but the second and third example fail? Is the first function expression getting hoisted somehow? And if so, why aren't the other 2 function expressions being hoisted? Or is this just some quirk with how function references work in JavaScript?

Brian K
  • 548
  • 1
  • 4
  • 17

2 Answers2

4

In your second and third examples, you are attempting to use logSomething before it is defined. This results in a type error.

The first example works, because the anonymous function passed into useEffect isn't immediately called, and so it's okay that logSomething isn't defined yet. By the time that function is called, const logSomething has been defined and is in scope.

Benjamin
  • 3,428
  • 1
  • 15
  • 25
2

This behavior is due to the design of JavaScript itself, and has nothing to do with React or the useEffect hook.

Within a block, both let and const are hoisted, but are initialized only when the corresponding declaration statements are executed. The period between the hoisting and the initialization is commonly referred to as the 'temporal dead zone (TDZ)'. Attempting to access variables in the TDZ raises a ReferenceError.

In your second and third example, although logSomething is declared within the same block, you attempt to access them (Either by attempting to run the assigned function, or pass the assigned function's reference to useEffect) before the initialization statement is executed, and thus you get an error. In the first example however, logSomething is accessed within the function passed to useEffect and by the time the function is executed, logSomething is already initialized. Thus, it works.


TL; DR

The first example works, because:

  1. logSomething is declared within the same block scope as the function passed to useEffect, and is thus available within scope.
  2. By the time the effect runs, logSomething declaration statement has already been executed, and thus you do not get a ReferenceError when you try to access logSomething (Unlike in the other two cases)

Here's an excellent answer if you'd like to learn more about the TDZ.

hexbioc
  • 1,586
  • 4
  • 13
  • Ok, so the main reason why the first example works is because useEffect is asynchronous in nature, and by the time it tries to call the arrow function I gave to it, the logSomething method exists. The other two examples fail because the logSomething function expression is currently in the "temporal dead zone", causing them to throw a TypeError when we attempt to access them within the block in the function. It would probably work the same way if "useEffect" was "setTimeout" or some other asynchronous method as well. That makes sense, thanks! – Brian K Oct 08 '20 at 21:22
  • 1
    Absolutely. In fact, this would also work if say you created a function `foo` within the block instead of calling `useEffect`, then declare `logSomething`, and call `foo` in the next statement in the *same* block. You would not get an error here either, as `logSomething` would have been initialized by the time `foo` runs. – hexbioc Oct 08 '20 at 21:25