skip to Main Content
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

see example here

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


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

      const [counter, setCounter] = useState(0);
      const counterRef = useRef(counter);
    
      // Update the ref whenever the counter changes
      useEffect(() => {
        counterRef.current = counter;
      }, [counter]);
    
      useEffect(() => {
        const intervalId = setInterval(() => {
          setCounter((prevCounter) => prevCounter + 1);
        }, 1000);
    
        return () => clearInterval(intervalId);
      }, []);
    
     useEffect(()=>{                                              
      showModal({
      title: "Confirmation",
      key: `modal-${counter}-${Date.now()}`,
      content: () => (
        <div>
          <p>Counter Value: {counterRef.current}</p>
        </div>
      ), 
      size: "xl",
      intent: "danger",
      primaryLabel: "Confirm",
      secondaryLabel: "Cancel",
      onPrimaryAction: () => console.log("Confirmed!"),
      onToggle: (isOpen) => setIsModalOpen(isOpen),
    })}, [counter])
    
    Login or Signup to reply.
  2. 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.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search