0

I guess it should come as no surprise that there are already so many questions that speak to this, however, each use case depends heavily on context so it is difficult to apply what others have experienced in attempting to solve my own issue.

Despite having tread ever so carefully in an attempt to write components that only re-render when absolutely necessary in a large React application with dozens of components and complex state, i have found it to be practically impossible to control causing unintended execution of the useEffect hook in several of my components.

That is to say, unless i remove a useEffect's dependencies when this becomes an issue, however, React's documentation does not explicitly state whether or not including all of a useEffect's dependencies is absolutely required.

The only hint that speaks to the fact that all of a useEffect's dependencies must be included is the fact that my IDE complains if i attempt to exclude one or all of them in-order to prevent a useEffect from executing when it should not or in-order to enforce a single execution.

This is a never ending rabbit hole that is costing me dozens of hours of backtracking and troubleshooting, and so id like to propose the following question in an attempt to get a definitive answer once and for all:

Is it ever wrong to exclude dependencies from a useEffect hook as a means of preventing it from executing more than once?

Quite frankly, this has been beyond frustrating and all of this time wasted has caused me to seriously consider ditching React altogether, despite its popularity, for Angular. For what it is worth i have never run into issues like this at all in Angular. The component life cycle in Angular is clear and simple to control.

UPDATE in response to Nathan below, i'll share one VERY simple example of a useEffect hook in my application and my approach to controlling its execution without removing it's dependencies.

Some context. authStatus and uiStatus are references to state in a React Redux store.

The hook before i took control over its execution:

...
  const authStatus = useSelector((state:any) => state.auth);
  const uiStatus = useSelector((state:any) => state.interface);
 
  const [frontCameraEnabled, setFrontCameraEnabled] = useState<boolean>(false);
    ...
    useEffect(() => {
      if(authStatus.isAuthenticated) {
        switch(uiStatus.device.operatingSystem) {
          case 'ios':
            setFrontCameraEnabled(true);
          break;
        }
        dispatch(cameraCheckPermissions());
      }
    }, [dispatch, authStatus.isAuthenticated, uiStatus.device.operatingSystem]);

Now — immediately I am well aware of the fact that updating the frontCameraEnabled state will cause my component to re-render, and so to prevent this logic from executing a second time when useEffect executes again as a result, i do the following which seems insane to me to have to take this kind of approach in dozens of places throughout my application:

OUTSIDE of my component i define a boolean:

let initalChecksComplete:boolean = false;

and then INSIDE my useEffect hook:

useEffect(() => {
      if(!initalChecksComplete && authStatus.isAuthenticated) {
        initalChecksComplete = true;
        switch(uiStatus.device.operatingSystem) {
          case 'ios':
            setFrontCameraEnabled(true);
          break;
        }
        dispatch(cameraCheckPermissions());
      }
    }, [dispatch, authStatus.isAuthenticated, uiStatus.device.operatingSystem]);

This just feels wrong especially in places where the logic is actually complex. I feel like if the useEffect hook is designed to execute more than once to begin with then the React team should also provide a hook that is designed to execute ONCE and ONLY once when a component initially mounts/renders.

DevMike
  • 1,630
  • 2
  • 19
  • 33
  • Not an exact answer to your question, but [this article](https://overreacted.io/a-complete-guide-to-useeffect/) is fantastic dive into understanding useEffect and why/how it works. Highly recommend, as it might also firm up some React concepts and mental models post Hooks. Also, it covers your question. – Brendan Bond Aug 06 '22 at 06:30
  • Briefly reading your edit - "immediately I am well aware of the fact that updating the frontCameraEnabled state will cause my component to re-render, and so to prevent this logic from executing a second time when useEffect executes again as a result" What makes you think the effect will run again? If none of the dependencies have changed (and it appears to me you've only set a new value for the `frontCameraEnabled` state) the effect won't run again. This is the purpose of the dependencies array - to "diff" your effects so they won't run unnecessarily. – Brendan Bond Aug 06 '22 at 07:36
  • Brendan — YES my thoughts exactly, but to test that theory i thew a console.log into the hook INSIDE the if statement that checks for authentication and it clearly prints more than once during runtime when I know for a fact that neither of the redux store references have changed. HOW do i troubleshoot? I am currently under the impression that my component WILL re-render if local or prop state changes in my component. – DevMike Aug 06 '22 at 07:45
  • "I am currently under the impression that my component WILL re-render if local or prop state changes in my component." Yes, it definitely will. There's a distinction here between "my component will rerender" and "my component's effect will rerun." Try and grok that article I linked in my first comment, I think it will REALLY shed some light on some of your issues here. – Brendan Bond Aug 06 '22 at 07:51
  • If we're still talking effects, it's hard to say why the effect may be running more than once when the dependencies haven't changed. I'd start troubleshooting [here](https://stackoverflow.com/a/60619061/4756341) – Brendan Bond Aug 06 '22 at 07:53
  • Thank you for pointing out the distinction (rerender vs effect rerun) — and thank you for the link - definitely useful although it speaks exactly to why i have considered ditching react all together. I am having to CONSTANTLY backtrack "up the tree" to find out the "cause" of re-renders / re-execution which is beyond debilitating. In Angular this sort of thing is just so simple to control and clear to understand. – DevMike Aug 06 '22 at 07:57
  • Also, just looking a little closer at your code (I know this is a minimal example, so I could be off base here) - what exactly is the purpose of a useEffect hook here? You're checking bits of state to derive another bit of state. The purpose of an effect is to sync things that are *outside of the React data flow* with props and state. You may consider [deriving](https://kentcdodds.com/blog/dont-sync-state-derive-it) here? – Brendan Bond Aug 06 '22 at 08:09
  • 2
    Lastly, I feel your pain - people new to React often have these problems. One super common anti-pattern for folks new to React is to massively overuse the useEffect hook, for example. If you keep trying to be "Thinking in React" (not my term) - things like minimal state, data flow as unidirectional as possible, etc. etc. - you'll eventually start getting past these problems and really be utilizing React for its strengths. Best of luck! – Brendan Bond Aug 06 '22 at 08:12
  • Yes, it is a minimal example and really this particular example is not much of an issue for me, but to have to share some of the more complex places where i have useEffect hooks that execute when they should not would require sharing many multiple parent components props and other state in-order for it to be clear that the dependencies have not changed. – DevMike Aug 06 '22 at 08:13
  • Hence the question - simply put - is it wrong to exclude dependencies - but i now know that yes, it would be wrong to exclude them. I have not given up ... yet. ;) – DevMike Aug 06 '22 at 08:15
  • 1
    Some argue that the dependency array should contain all dependent values, but I've never seen the use for it - it can make the logic too convoluted. I reasonably often skip dependencies in `useEffect` despite linter warnings. (But other hooks like `useMemo` and `useCallback` *should* have all dependencies in the dependency array - but that's a lot easier to manage because they shouldn't cause unintended problems, unlike `useEffect`) – CertainPerformance Aug 07 '22 at 22:32

1 Answers1

0

If you depend on it, it should be in the dependency array. The IDE is complaining for a reason.

Don't get discouraged though, and (I'm biased, but) don't ditch React! It takes some getting used to useEffect, but it's very powerful. Without knowing your use-case or seeing examples, I can't really provide advice, but keep in mind that useEffect being executed doesn't mean all the logic within it needs to be executed-- you can use if statements and such to execute only when the values you care about change.

Do you have an example of something that was causing you issues?

Nathan
  • 73,987
  • 14
  • 40
  • 69
  • Yes, i had intended on sharing some code, but quickly realized that id have to include so much of the application in-order for it to be clear why i have an issue. i.e. props and methods inside parent components that are likely causing the child component to re-render. Hence my frustration. I can try to share some code, ill add it now, and it would be great to get some feedback. – DevMike Aug 06 '22 at 06:45
  • I've included a very basic example. – DevMike Aug 06 '22 at 07:30
  • `the React team should also provide a hook that is designed to execute ONCE and ONLY once when a component initially mounts/renders.` just provide an empty depdency array, []. That will only run after the first render. Does that solve your problem? – Nathan Aug 06 '22 at 07:58
  • an empty array *would* solve the problem yes, but this goes against best practices. The useEffect hook requires its dependancies to function as intended. – DevMike Aug 06 '22 at 08:00
  • 1
    Ah, I think I see the misunderstanding. It doesn't go against best practices if you only want to run it once, read [the documentation here](https://reactjs.org/docs/hooks-effect.html). "all values from the component scope (such as props and state) that change over time and that are used by the effect. Otherwise, your code will reference stale values from previous renders". If you want it to run only once on startup, use an empty dependency array. If you want to run it whenever something changes, include that in the dependency. In your example, how often do you want it to run? – Nathan Aug 06 '22 at 08:29
  • Do you want it to run again when authentication changes, or only run once on load? – Nathan Aug 06 '22 at 08:29
  • My initial answer seems to be wrong, I didn't understand your use cases. – Nathan Aug 06 '22 at 08:29
  • Actually, i think your answer is correct. "If you depend on it, it should be in the dependency array. The IDE is complaining for a reason." – DevMike Aug 06 '22 at 18:46
  • I tried to use a simple example since most of my useEffect hooks contain a lot of dependencies, sharing any of the others would be difficult. Really i am just trying to find best practice for preventing a useEffect from executing despite one of its dependencies changing. It would be ideal if there was a hook that is designed to run only once like the former "componentDidMount" but we don't have it. – DevMike Aug 06 '22 at 18:49
  • In the example i used, the effect is running more than once already even WITHOUT any authentication change. None of dependencies changed and yet a console log shows that the if statement executes more than once. Very frustrating. – DevMike Aug 06 '22 at 18:51
  • Is it executing twice? In development, Apps are usually wrapped in a "StrictMode" which calls use effect twice, to help find bugs. See my other answer: https://stackoverflow.com/a/73261944/954986 – Nathan Aug 06 '22 at 19:57
  • I am actually looking at Strict mode as we speak. This may be the source of my issue. I also feel like i could have proposed this question in a better format, and thus i asked a new question. I am updating this one to direct people there instead. – DevMike Aug 06 '22 at 20:09
  • 1
    Okay, a few things from your updated question. 1) you don't need to have `dispatch` in the dependency array, since that never changes.2) you pull the `uiStatus` from Redux, but you use `uiStatus.device.operatingSystem` in the dependency array. That *might* cause useEffect to get fired any time `uiStatus` changes, not just when os changes. Try adding another `const operatingSystem = useSelector(...)` to pull just that state. 3) initalChecksComplete won't work, as the boolean gets reset each render, you'd need to use useRef instead. – Nathan Aug 06 '22 at 20:26
  • 1
    But useEffect should *only* fire when any of its dependencies change, or once if it uses an empty dependency array. The only time this isn't true is if you're wrapping everything in StrictMode, which you *can* remove if it bothers you. – Nathan Aug 06 '22 at 20:27