Calling a React hook conditionally
Asked Answered
M

4

49

From the react official documentation we know that "React relies on the order in which Hooks are called". So is there anything wrong with "reserving" a spot for a hook if I want to call it conditionally?

function Component({flag, depA, depB}) {

  if (flag) {
    // just "reserving a spot"
    useEffect(() => {}, [null, null])
  } else {
    useEffect(() => {
      // ... actual hook
    }, [depA, depB])
  }

  return <></>
}

If this works, would it also work for useCallback? useLayoutEffect? useMemo? useImperativeHandle?

I've tested all of this and in much more complicated contexts, it seems to work even though the linter complains. Am I missing something?

PS: if it looks kind of useless just like this, it's because the end goal is to have the main part of the hook be lazy loaded with import(), and before the import is triggered and resolved, just reserve the spots for hooks.

Monkhood answered 3/6, 2021 at 19:3 Comment(1)
please see the documentation reactjs.org/docs/hooks-rules.html, you are not able to call a hook conditionally like you are doingLorant
L
28

I was just thinking about this today. I believe that while it 'breaks the rules', there's nothing that React could do to tell the difference between the two.

So while it breaks the rules, if you have a good enough reason, understand the risks, then the 'rules' is just dogma.

React basically knows which useEffect hook is which, by counting invocations. Calling useEffect conditionally is bad, specifically because the amount of times useEffect gets called cannot change.

Your example is conditional, but React can't detect it because in either condition you call it once.

However, the example you mention seems like it doesn't need this. There's good reasons to do things the 'normal' way, because as you can see from other commenters here, it causes confusion and surprise, and we don't like surprise =)

If you are lazily loading in some functionality, just have your useEffect hook call the function when it is ready.

Lowrey answered 5/6, 2021 at 3:15 Comment(1)
I think we're on the same page! My exemple is contrived for the question, but what I really want to do it to load an entire custom hook, with many built-in hooks inside. I just "finished" a package that does just that npmjs.com/package/use-imported-hook.Monkhood
M
26

My solution is like this, and it works for all hooks, however it's better to use it when you have to since it can cause confusion and surprise

const ConditionalEffect1 = () => {
  useEffect(() => {}, [])
  return null
}

const ConditionalEffect2 = ({depA, depB}) => {
  useEffect(() => {}, [depA, depB])
  return null
}

const Component = ({ flag, depA, depB }) => {
  return flag 
          ? <ConditionalEffect1 /> 
          : <ConditionalEffect2 depA={depA} depB={depB} />
}

Maricamarice answered 11/8, 2022 at 12:57 Comment(0)
L
12

You cannot call useEffect conditionally as that is breaking the rules of hooks instead you could do the following:

  useEffect(() => {
    if (flag) {
      console.log("do something");
    } else {
      console.log("do something else");
    }
  }, [depA, depB]);
Lorant answered 3/6, 2021 at 19:8 Comment(6)
Yes but that's what the doc says, it doesn't mean that's what it does under the hood. If I were to write doc for a hugely popular library, i'd rather be overly cautious in my wording too.Monkhood
The explanation from the doc is the following: By following this rule, you ensure that Hooks are called in the same order each time a component renders. That’s what allows React to correctly preserve the state of Hooks between multiple useState and useEffect calls.Monkhood
I could see how this is possible if you are using a custom build of react as opposed to CRA. I don't think the above code would even compile correctly in CRALorant
In my example, I am also calling hooks in the same order each time a component renders.Monkhood
I have tested in a bunch of complex CRA and Next.js apps, it works, I'm just not sure if it's lucky and it's going to break soon, or if it's compatible w/ the underlying logic.Monkhood
understood, in my experience using CRA if I were to attempt something along those lines the application would crash for me.Lorant
G
-1
function useValues(isPdf: boolean) {
  const useGetValues = isPdf ? usePdfValues : useRegularValues;
  return useGetValues();
}
Gust answered 11/4 at 12:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.