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
}
⚠ 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
}
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
}
}
// 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.
Top comments (2)
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.Interesting approach, but I think there are a few problems with this the way you wrote it:
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 propscondition
will change the react render tree structure and thus cause all children to re-mountoutput
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 MyFCMaybe you could mitigate most of these issues with a render structure like this:
There's also a third option of ignoring the lint rule if you're sure that the condition never changes.
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.