skip to Main Content

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.

3

Answers


  1. 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.

    Login or Signup to reply.
  2. While adding the dependency is one of the right ways to do it, if you do not want it to only be triggered when open changes to true, you can do track the previous value to make the correct updates:

    Keep a ref to

     const ref = useRef();
    
     /** your other effect **/
    useEffect(() => {
        if (open && !ref.current) {
          onOpen()
        }
      }, [open,onOpen])
    
    
      useEffect(() => {
        ref.current = open;
      });
    

    Here ref is a ref that holds the value of open from previous render and in the useEffect you can check whether that value was falsy earlier and true right now.

    This functionality can also be moved to a hook

    Login or Signup to reply.
  3. You can achieve this by ensuring the callback (onOpen) is triggered every time the open prop becomes true, and making sure that it doesn’t trigger unnecessarily when the open prop is already true.

    const MyDialog = ({ open, onOpen, children }) => {
      React.useEffect(() => {
        if (open) {
          onOpen();
        }
      }, [open, onOpen]);  
    
      return (
        <UnderlyingDialogComponent open={open}>
          {children}
        </UnderlyingDialogComponent>
      );
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search