reactjsreact-hooks

How to call a callback prop when another prop changes without omitting useEffect dependencies?


I have a simple dialog component that takes an open prop to control whether the dialog is shown or not, and an onOpen prop which is a callback to be run every time the dialog opens.

I can achieve this very simply like this:

const MyDialog = ({ open, onOpen, children }) => {
  React.useEffect(() => {
    if (open) {
      onOpen()
    }
  }, [open])


  // The dialog that gets rendered uses a UI library’s dialog component.
  return (
    <UnderlyingDialogComponent open={open}>
      {children}
    </UnderlyingDialogComponent>
  )
}

This works perfectly fine, except for the fact that the react-hooks/exhaustive-deps linting rule (from eslint-plugin-react-hooks) tells me that I need to include onOpen in the dependency array for the useEffect.

If I do that however, then the onOpen callback also gets called whenever the onOpen callback changes, which is not the behaviour that I want. I want it only to be called when the open prop changes from false to true.

Is there a simple way to do this without just disabling the linting rule? Or is this a situation where I can ignore the “rule” that all dependencies must be included in the dependency array?

Edit: I am aware that if the onOpen handler is made using useCallback then it shouldn't change, but I can't guarantee that any user of this component will do that. I would like my component to function correctly regardless of whether the user has used useCallback for their onOpen handler.


Solution

  • You can consider a custom hook, useEffectEvent to help you handle this. React is currently experimenting with a feature like this, so you can also import it with:

    import { experimental_useEffectEvent as useEffectEvent } from 'react';
    

    or, you can create something similar which you maintain yourself by using a polyfill such as this one.

    This hook will return a stable function reference across all re-renders, so it doesn't need to be included in your dependency array. It also has the added advatnage of always calling the "latest" onOpen function, avoiding potential stale closure issues within that callback. You can use it like so in the MyDialog component:

    const MyDialog = ({ open, onOpen, children }) => {
      const onOpenHandler = useEffectEvent(onOpen);
      
      React.useEffect(() => {
        if (open) {
          onOpenHandler();
        }
      }, [open]);
    
      ...
    }
    

    Keep an eye out for the current limitations with the callback you get back from this hook. Also, it's worth considering if you need an effect here in the first place. For example, if your inner dialog has an onOpen event you can tap into, as using that would be better to call your onOpen callback.