Svetlana Ivanova
Svetlana Ivanova

Reputation: 155

React hooks. Update component passed as param via onClick after one of it's prop was changed

Hi guys) I have a strange question may be, but I'm at a dead end.

I have my own custom hook.

 const useModal = (Content?: ReactNode, options?: ModalOptions) => {
  const { isOpen, close: contextClose, open: contextOpen, setContent } = useContext(
    ModalContext,
  )

  const [customOpenContent, setCustomOpenContent] = useState<ReactNode>()

  const showModal = useCallback(
    (customContent?: ReactNode) => {
      if (!isNil(customContent)) {
        setCustomOpenContent(customContent)
        contextOpen(customContent, options)
      } else contextOpen(Content, options)
    },
    [contextOpen, Content, options],
  )

  const hideModal = useCallback(() => {
    contextClose()
  }, [contextClose])

  return { isOpen, close: hideModal, open: showModal, setContent }
}

It is quite simple. Also i have component which uses this hook

const App: React.FC = () => {

  const [loading, setLoading] = useState(false)

  const { open } = useModal(null, { deps: [loading] })

  useEffect(() => {
    setTimeout(() => {
      setLoading(true)
    }, 10000)
  })

  const buttonCallback = useCallback(() => {
    open(<Button disabled={!loading}>Loading: {loading.toString()}</Button>)
  }, [loading, open])

  return (
    <Page title="App">
      <Button onClick={buttonCallback}>Open Modal</Button>
    </Page>
  )
}

Main problem is - Button didn't became enabled because useModal hook doesn't know anything about changes.

May be you have an idea how to update this component while it's props are updated? And how to do it handsomely ))

Upvotes: 2

Views: 436

Answers (1)

Zachary Haber
Zachary Haber

Reputation: 11027

Context isn't the best solution to this problem. What you want is a Portal instead. Portals are React's solution to rendering outside of the current React component hierarchy. How to use React Portal? is a basic example, but as you can see, just going with the base React.Portal just gives you the location to render.

Here's a library that does a lot of the heavy lifting for you: https://github.com/wellyshen/react-cool-portal. It has typescript definitions and provides an easy API to work with.

Here's your example using react-cool-portal.

import usePortal from "react-cool-portal";

const App = () => {
  const [loading, setLoading] = useState(false);
  const { Portal, isShow, toggle } = usePortal({ defaultShow: false });

  useEffect(() => {
    setTimeout(() => {
      setLoading(true);
    }, 10000);
  });

  const buttonCallback = useCallback(() => {
    toggle();
  }, [toggle]);

  return (
    <div title="App" style={{ backgroundColor: "hotpink" }}>
      <button onClick={buttonCallback}>
        {isShow ? "Close" : "Open"} Modal
      </button>
      <Portal>
        <button disabled={!loading}>Loading: {loading.toString()}</button>
      </Portal>
      <div>{loading.toString()}</div>
    </div>
  );
};

Basic CodeSandbox Example

There are more detailed ones within the react-cool-portal documentation.


For more detail of the issues with the Context solution you were trying, is that React Elements are just a javascript object. React then uses the object, it's location in the tree, and it's key to determine if they are the same element. React doesn't actually care or notice where you create the object, only it's location in the tree when it is rendered.

The disconnect in your solution is that when you pass the element to the open function in buttonCallback, the element is created at that point. It's a javascript object that then is set as the content in your context. At that point, the object is set and won't change until you called open again. If you set up your component to call open every time the relevant state changes, you could get it working that way. But as I mentioned earlier, context wasn't built for rendering components outside of the current component; hence why some really weird workarounds would be required to get it working.

Upvotes: 1

Related Questions