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>
)
}
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
}
}
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;
};
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]
);
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)