DEV Community

Cover image for Conditional hooks?
Alex Lohr
Alex Lohr

Posted on • Updated on

Conditional hooks?

One thing you'll find out early adopting react is that you cannot have conditional hooks. This is because every hook is initially added into a list that is reviewed on every render cycle, so if the hooks don't add up, there is something amiss and any linter set up correctly will warn you.

const useMyHook = () => console.log('Hook is used')

type MyProps = { condition: boolean }

const MyFC: React.FC<MyProps> = ({ condition }) => {
  if (condition) {
    useMyHook()
  }
  return null
}
Enter fullscreen mode Exit fullscreen mode

⚠ React Hook "useRef" is called conditionally.
React Hooks must be called in the exact same order
in every component render. (react-hooks/rules-of-hooks)

However, there are two patterns to allow for something that does the same job as a hook that would only be executed when a condition is met.

Conditionally idle hook

One possibility is to make the hook idle if the condition is not met:

const useMyConditionallyIdleHook = (shouldBeUsed) => {
  if (shouldBeUsed) {
    console.log('Hook is used')
  }
}

type MyProps = { condition: boolean }

const MyFC: React.FC<MyProps> = ({ condition }) => {
  useMyConditionallyIdleHook(condition)

  return null
}
Enter fullscreen mode Exit fullscreen mode

This is fine if you can rely on useEffect and similar mechanisms to only trigger side effects if the condition is met. In some cases, that might not work; you need the hook to be actually conditional.

The conditional hook provider

A hook is only ever called if the parent component is rendered, so by introducing a conditional parent component, you can make sure the hook is only called if the condition is met:

// use-hook-conditionally.tsx
import React, { useCallback, useRef } from 'react'

export interface ConditionalHookProps<P, T> {
  /**
   * Hook that will only be called if condition is `true`.
   * Arguments for the hook can be added in props as an array.
   * The output of the hook will be in the `output.current`
   * property of the object returned by `useHookConditionally`
   */
  hook: (...props: P) => T
  /**
   * Optional array with arguments for the hook.
   *
   * i.e. if you want to call `useMyHook('a', 'b')`, you need
   * to use `props: ['a', 'b']`.
   */
  props?: P
  condition: boolean
  /**
   * In order to render a hook conditionally, you need to
   * render the content of the `children` return value;
   * if you want, you can supply preexisting children that
   * will then be wrapped in an invisible component
   */
  children: React.ReactNode
}

export const useHookConditionally: React.FC<ConditionalHookProps> = ({
  hook,
  condition,
  children,
  props = []
}) => {
  const output = useRef()

  const HookComponent = useCallback(({ children, props }) => {
    output.current = hook(...props)
    return children
  }, [hook])

  return {
    children: condition
      ? <HookComponent props={props}>{children}</HookComponent>
      : children,
    output
  }
}
Enter fullscreen mode Exit fullscreen mode
// component-with-conditional-hook.tsx
import React from 'react'
import { useHookConditionally } from './use-hook-conditionally'

const useMyHook = () => 'This was called conditionally'

type MyProps = { condition: boolean }

const MyFC: React.FC<MyProps> = ({ condition, children }) => {
  const { output, children: nodes } = useConditionallyIdleHook({ 
    condition,
    hook: useMyHook,
    children
  })

  console.log(output.current)
  // will output the return value from the hook if
  // condition is true

  return nodes
}
Enter fullscreen mode Exit fullscreen mode

For this to work, you need to render the children, otherwise the hook will not be called.

Discussion (2)

Collapse
alexgulmi profile image
Alexander Gundermann • Edited on
  1. I think that's the best approach. Your example could be improved though, because if you now try to use a hook inside the shouldBeUsed you will run into the same problem. You would have to make sure not to attach any listeners, call setState etc, which depends a lot on what the hook actually does.

  2. Interesting approach, but I think there are a few problems with this the way you wrote it:

  • you create a different HookComponent on every render, thus causing everything to re-mount. Could be mitigated by putting the component and the latest props into a ref, or by making it a stand-alone component that takes the ref, hook props, and hook function via props
  • toggling condition will change the react render tree structure and thus cause all children to re-mount
  • the hook only runs after the main component MyFC is rendered, since it'll be deeper in the react tree, so output should be one render cycle behind. I guess that's okay if the hook doesn't return anything. In other cases, I think you'd have to re-structure it so that the hook component is a parent of MyFC

Maybe you could mitigate most of these issues with a render structure like this:

<HookWrapper>
  <HookComponent /> // conditionally rendered
  <MyFC />
</HookWrapper>
Enter fullscreen mode Exit fullscreen mode

There's also a third option of ignoring the lint rule if you're sure that the condition never changes.

Collapse
lexlohr profile image
Alex Lohr Author

Thanks for your comment. Yes, an issue with 1. caused me to consider 2. - in any case, I found a work around for 1., but didn't want the thought go to waste.

Also thanks for the hint, I changed the code to now memoize the component.

I did not yet publish a package, but wanted to publish this a POC. I'll have a look into the wrapper structure some time later.