reactjs / rfcs

RFCs for changes to React

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

[hooks][eslint] Add an exception in calling hooks conditionally

osdnk opened this issue · comments

Hi,
Firstly, I'd like to say that I'm not going to encourage anyone to make the hooks' calling cycle actually conditional depending on the dynamic state of component or application.

Let me present an example from the React Native application when we're using react-hooks/rules-of-hooks

export const useBackHandler = handler => {
  const savedHandler = useRef()

  useEffect(() => {
    savedHandler.current = handler
  }, [handler])

  useEffect(() => {
    const eventListener = event => {
      savedHandler.current(event)
      return true
    }

    BackHandler.addEventListener('hardwareBackPress', eventListener)

    return () => {
      BackHandler.removeEventListener('hardwareBackPress', eventListener)
    }
  }, [])
}

This behavior is valid only on the Android platform so basically I don't want to call these lines on web neither on iOS. Of course, it's not heavy thing but it's just an example.

However, adding

export const useBackHandler = handler => {
  if (!IS_ANDROID) {
    return
  }

or

  if (IS_ANDROID) {
    useBackHandler()
  }

breaks an eslint rule.

My approach, in this case, is moving things to separated files and using ios.js and .android.js extensions (what's often tedious) or wrapping inside the component and render conditionally (what's ugly) or disabling rule on each call (what's ugly and potentially dangerous).

What do you think about adding an exception for constants (maybe using capitalize letters like IS_ANDROID) which can lead to the conditional calling of hooks?

I think there're some use cases sharing these approaches. Probably in other languages, people will use preprocessing for these situations (platform constants, disabling features, config of the app), but we're not commonly using this mechanism in JavaScript.

I have to agree, IS_DEVELOPMENT and IS_PRODUCTION would be other good candidates for this. Essentially everything that is decided at build time.

Maybe a default exception for everything that starts with process.env, along with a configuration option to specify own values?

When you move the condition out of the function body everything works without eslint complaining (and the unused code path can even be removed by the compiler).

const noop = () => {}
export const useAndroidBackHandler = IS_ANDROID ? (handler) => {
  const savedHandler = useRef()

  useEffect(() => {
    savedHandler.current = handler
  }, [handler])

  useEffect(() => {
    const eventListener = event => {
      savedHandler.current(event)
      return true
    }

    BackHandler.addEventListener('hardwareBackPress', eventListener)

    return () => {
      BackHandler.removeEventListener('hardwareBackPress', eventListener)
    }
  }, [])
} : noop

@xiel Well, yes, but IMHO it's too much just to satisfy the eslint constraint.

Idea of moving "condition" from inside to outside is actually quite good. Some hoc/wrappers could make it really elegant. The problem here is tree-shaking - it will just stop working once you hide condition behind some syntax sugar.

Ideally eslint rule should allow conditions based on any const variable. Because they are not going to change.

commented

Hi, thanks for your suggestion. RFCs should be submitted as pull requests, not issues. I will close this issue but feel free to resubmit in the PR format.