DEV Community

illia chaban
illia chaban

Posted on

What do you think about hooks returning components?

Let's say we have a UI component Modal that needs some stateful logic to be instantiated every time we use it

const ComponentThatUsesModal = () => {
  const [visible, setVisible] = useState(false)
  const openModal = useCallback(() => setVisible(true), [setVisible])
  const closeModal = useCallback(() => setVisible(false), [setVisible])

  return (
    <div>
      Lorem Ipsum some text
      <Button onClick={openModal}>
        Show more information
      </Button>

      <Modal
        open={visible}
        onClose={closeModal}
      >
        <p>
          More information here
        </p>
      </Modal>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

We could reuse that stateful logic by using useModal hook, but then we would need to import both useModal and Modal + pass in props that are specific to modal (visible, closeModal) every single time. Ideally, we'd want avoid exposing those props since they are not used outside the modal itself (because closing logic is fully handled by modal). Wouldn't it be nice if we could do smth like this:

const ComponentThatUsesModal = () => {
  const {Modal, openModal} = useModal()

  return (
    <div>
      Lorem Ipsum some text
      <Button onClick={openModal}>
        Show more information
      </Button>

      <Modal>
        <p>
          More information here
        </p>
      </Modal>
    </div>
  )
}

// hooks/use-modal
const useModal = () => {
  const [open, setOpen] = useState(false)
  const openModal = useCallback(() => setOpen(true), [setOpen])
  const onClose = useCallback(() => setOpen(false), [setOpen])

  const Modal = useComponent(ModalBase, {open, onClose})

  return {
    openModal,
    Modal
  }
}
Enter fullscreen mode Exit fullscreen mode

Here's the implementation of the useComponent hook

const useComponent = (Component, props = {}) => {
  const propsRef = useRef(props);
  propsRef.current = props;

  const componentRef = useRef((other) => {
    // It will use the very first Component passed into the hook
    return <Component {...propsRef.current} {...other} />;
  });

  return componentRef.current;
};
Enter fullscreen mode Exit fullscreen mode

This works. Check this sandbox. However, my concern is that I don't understand how it works. How does the component know to update if we're keeping track of original binded props through ref ? There's a second implementation using Subject from Rxjs:

const useComponentV2 = (Component, bindedProps = {}) => {
  const propsSubjectRef = useRef(new Subject());
  useEffect(() => {
    propsSubjectRef.current.next(bindedProps);
  }, [bindedProps]);

  const componentRef = useRef((other) => {
    const [props, setProps] = useState(bindedProps);

    const currentPropsRef = useRef(props);
    currentPropsRef.current = props;

    useEffect(() => {
      const subscription = propsSubjectRef.current.subscribe((newProps) => {
        if (shallowEqual(newProps, currentPropsRef.current)) return;
        setProps(newProps);
      });
      return () => subscription.unsubscribe();
    }, []);
    // It will use the very first Component passed into the hook
    return <Component {...props} {...other} />;
  });

  return componentRef.current;
};

const shallowEqual = (obj1, obj2) =>
  Object.keys(obj1).length === Object.keys(obj2).length &&
  Object.keys(obj1).every(
    (key) => obj2.hasOwnProperty(key) && obj1[key] === obj2[key]
  );
Enter fullscreen mode Exit fullscreen mode

It does make it rerender twice comparing to the first implementation, but at least I can clearly see what makes it rerender (state change). Does anyone have any comments / concerns on the implementation? We feel like it would work really nice for our use case in production, but because it's so new and I haven't seen any documentation on it, I'm also afraid we can be shooting ourselves in the foot.

Thank you for all the responses!

Top comments (0)