Conditional hooks?

Alex Lohr - May 19 '21 - - Dev Community

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.

. . . . . . . . . . . . . . . . . . . . .
Terabox Video Player