const [counter, setCounter] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
setCounter((prevCounter) => prevCounter + 1);
}, 1000);
return () => clearInterval(intervalId);
}, []);
const handleOpenModal = () => {
showModal({
title: "Confirmation",
key: `modal-${counter}-${Date.now()}`,
content: () => (
<div>
<p>Counter Value: {counter}</p>
</div>
),
size: "xl",
intent: "danger",
primaryLabel: "Confirm",
secondaryLabel: "Cancel",
onPrimaryAction: () => console.log("Confirmed!"),
onToggle: (isOpen) => setIsModalOpen(isOpen),
});
};
the two code blocks above are part of my parent component
type ModalProps = {
isVisible: boolean;
onPrimaryAction?: () => void;
onClose?: () => void;
onSecondaryAction?: () => void;
onToggle?: (isOpen: boolean) => void;
title?: string;
intent?: Intent;
primaryLabel?: string;
secondaryLabel?: string;
size?: "sm" | "default" | "lg" | "xl";
content?: ReactNode | ((...args: any[]) => ReactNode);
key?: string;
};
const ModalContext = createContext<{
showModal: (props: Partial<ModalProps>) => void;
hideModal: () => void;
} | null>(null);
function ModalProvider({ children }: { children: ReactNode }) {
const [isVisible, setIsVisible] = useState(false);
const [modalProps, setModalProps] = useState<Partial<ModalProps>>({});
const showModal = useCallback((props: Partial<ModalProps> = {}) => {
setModalProps(props);
setIsVisible(true);
}, []);
const hideModal = useCallback(() => {
setIsVisible(false);
}, []);
return (
<ModalContext.Provider value={{ showModal, hideModal }}>
{children}
<Modal
{...modalProps}
isVisible={isVisible}
onClose={hideModal}
key={modalProps.key}
/>
</ModalContext.Provider>
);
}
export const useModal = () => {
const context = useContext(ModalContext);
if (!context) {
throw new Error("useModal must be used within a ModalProvider");
}
return context;
};
function Modal({
isVisible,
onPrimaryAction,
onClose,
onSecondaryAction,
onToggle,
title,
intent = "default",
primaryLabel,
secondaryLabel,
size = "default",
content,
}: ModalProps) {
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape" && isVisible) onClose?.();
};
if (isVisible) document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [isVisible, onClose]);
useEffect(() => {
onToggle?.(isVisible);
}, [isVisible, onToggle]);
return (
<AnimatePresence>
{isVisible && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose}
>
<motion.div
initial={{ y: 10 }}
animate={{ y: 0 }}
exit={{ y: 10 }}
transition={{ type: "spring", duration: 0.3, bounce: 0.25 }}
onClick={(e) => e.stopPropagation()}
>
<div>
<Text size="lead" weight="semibold">
{title || "Modal Title"}
</Text>
<button type="button" className="modal-btn" onClick={onClose}>
<IoCloseOutline />
</button>
</div>
<div className="flex-1 p-6">
{typeof content === "function" ? content() : content}
</div>
{(secondaryLabel || primaryLabel) && (
<div>
{secondaryLabel && (
<Button
variant="outline"
className="capitalize"
onClick={() => {
onSecondaryAction?.();
onClose?.();
}}
>
{secondaryLabel}
</Button>
)}
{primaryLabel && (
<Button
onClick={onPrimaryAction}
variant="solid"
intent={intent}
className="capitalize "
>
{primaryLabel}
</Button>
)}
</div>
)}
</motion.div>
</motion.div>
)}
</AnimatePresence>
);
}
export default ModalProvider;
now this is my modal component
the problem is that when I pass the counter (which is incrementing every second) to the content and trigger the modal open, it only gets the last state the counter was in, it doesnt update as the counter increments
this is only a test case for passing dynamic content to the modal’s content prop.
I tried re-rendering the modal using a useEffect and the value that onToggle is giving me, but that causes so many re-renders and not really the best way to do this.
what seems to be the problem and how can I fix it?
I want my modal component to take in dyanmic and/or static content, if the parent component has a state that changes occasionally, and that state was to be passed to the content prop of the modal, than while the modal is open it should update as well.
2
Answers
Your Modal component does not have the latest value of the content due to the stale closure. To avoid this u can store the latest counter value in a useRef, which allows you to access the current value without stale closures.
This happen because even though the state is updated, the pop up isn’t, hence the state is stale. useRef wouldn’t help because useRef will not trigger a re-render, its different than useState. Try making the handleOpenModal into a react component, and pass the counter as a prop. This way, everytime the state change, the modal will change as well.In the end the modal have to re-render everytime no matter what, but I think this approach is much better than the useEffect one, which can cause unnecessary re-render.