variables used in callback in useEffect are updated despite not being dependencies

  const comp_ref = useRef();
  const { active_page, path_arr, is_active_alert } = useCustomHook();
  const { user_loaded, nav_collapse, mobile } = useSelector(
    (state) => state.boot
  );

  useEffect(() => {
    if (nav_collapse || mobile) {
      if (is_active_alert && comp_ref.current) {
        comp_ref.current.style.transform = "translateY(0px)";
      }
    }
  }, [is_active_alert]);

In the useEffect, it does not have nav_collpase or mobile as its dependencies/triggers.

Both nav_collpase and mobile are redux states that hold merely boolean values.

Based on my testing, it still gets the updated values for both nav_collpase and mobile whenever the callback is run (due to a change in is_active_alert).

Question:

  1. How is the callback in the useEffect able to get the updated values for the state variables when it doesn't depend on them?

Upvotes: 0

Views: 76

Answers (4)

WeDoTheBest4You
WeDoTheBest4You

Reputation: 1954

Lexical scoping works in the same manner in both event handlers and useEffect handlers, therefore the answer to the question should already be known to you, let us refresh it.

Let us make a counter-question first.

While running the below sample code, on each click on the first button, the state randomnum is updated with a new random number. When the second button is clicked, it shows an alert of the latest random number.

Now there may be a question here. How does the handler handleAlertNumber get the latest value, even though it does not depend on the state randomnum ?

Let us write our right answer here. It gets the latest value because the state change triggers a new render, and during this new render, the previous version of the handleAlertNumber is destroyed and a new version has been re-created. This is correct.

However there is a point still remain unanswered, it is about the dependency. The handler has no dependency at all, still it got the latest value.

The improved answer may be, since handleAlertNumber does not have a dependency array, it gets redefined or re-created for all state changes in the component. This is absolutely correct.

App.js

import { useState } from 'react';

export default function App() {
  const [randomnum, setRandomNum] = useState(Math.random());

  function handleAlertNumber() {
    alert(randomnum);
  }

  return (
    <>
      <button onClick={() => setRandomNum(Math.random())}>
        Set a new random number
      </button>
      <br />
      <br />
      <button onClick={handleAlertNumber}>Show the random number</button>
    </>
  );
}

Now coming to your question

Let us compare, an event handler and a useEffect handler. For instance, handleAlertNumber the one we discussed above is an event handler, and a useEffect handler is the one we are discussing in this question.

The answer may be:

handleAlertNumber runs in response to a specific user interaction - button clicking. Similarly, yes can say similarly, useEffect runs in response to a commit phase. It means it runs automatically soon after rendering and commit phases of a component. Therefore both may be generally considered as event handlers run in response to the respective events.

There is a difference though. Both are redefined on different conditions. While handleAlertNumber re-created on all state changes in the component, a useEffect handler is re-created only on the changes in specific states set as its dependency.

Now the answer to the question may be :

The handleAlertNumber is getting redefined on all state changes, therefore it can access the latest state randomnum, this accessibility does not depend on its dependency, which should be very clear by now since this handler has no dependency at all.

The same happens in useEffect handler as well. Its accessibility of states in the enclosing function does not depend on the dependency array. In the nutshell, this accessibility of states irrespective of its dependency is technically called lexical scoping which is the defining characteristic of closures. It means all events handlers in a React component are closure, therefore it can access the state in its enclosing function.

Upvotes: -2

Muhammad Saqib
Muhammad Saqib

Reputation: 388

Actually, the updated value is always accessed because when the callback fires due to a dependency change, it contains a new reference in memory. I also tried with memorizing the useEffect callback using useCallback, and it worked. While the useEffect callback still fires every time the dependency changes, it doesn't access the updated values. However, it uses the values from the first render (the values it was "memorized" with).

Here's an example of what I tried:

const [count, setCount] = useState(0);

  const callback = useCallback(() => {
    console.log(count);
  }, []);

  useEffect(callback, [count]);

I’m not sure if this has a real-world use case, but it’s always good to have the conceptual idea behind it.

Upvotes: 0

Drew Reese
Drew Reese

Reputation: 203323

How is the callback in the useEffect able to get the updated values for the state variables when it doesn't depend on them?

  • The useEffect hook is called each and every render cycle
  • The callback function is redeclared each and every render cycle, with any new values closed over in scope.

It's only when the useEffect hook's dependencies change that the callback is actually called, thus seeing whatever the current values are that have been closed over in callback scope.

Upvotes: 3

AKX
AKX

Reputation: 169338

How is the callback in the useEffect able to get the updated values for the state variables when it doesn't depend on them?

Because if the effect does run, it'll see those values, whatever they are. It'll just not get automatically run if those values change if the values aren't in dependencies.

(For instance, in Strict Mode, effects may be run more than once.)

Use eslint-plugin-react-hooks to warn you about these.

In any case, this looks like bad use of effects to begin with.

Assuming comp_ref just points to a div, you could just do

const AlertComponent = () => {
  const { active_page, path_arr, is_active_alert } = useCustomHook();
  const { user_loaded, nav_collapse, mobile } = useSelector((state) => state.boot);

  const shouldTransform = (nav_collapse || mobile) && is_active_alert;
  return <div style={shouldTransform ? { transform: "translateY(0px)" } : null}>...</div>;
};

Upvotes: 2

Related Questions