Conditional hooks?
Tags: react, reactnative, javascript
Published: 2021-05-19 19:43:13

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.

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

type MyProps = { condition: boolean }

const MyFC: React.FC<MyProps> = ({ condition }) => {
  if (condition) {
    useMyHook()
  }
  return null
}
```
> ⚠ 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:

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

type MyProps = { condition: boolean }

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

  return null
}
```

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:

```tsx
// 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
  }
}
```

```tsx
// 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
}
```

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