Weird React Behaviour, lost on how to debug

Hey guys,

I've been losing hair over this and am a bit lost, hoping someone can help.

I have this modal component which I can call, await and get a value from:

import { Button } from "../buttons/Button";
import Modal from "./Modal";

type AsyncModalProps = {
  title: string;
  content: ReactNode;
  confirmText?: string;
  cancelText?: string;
};

type AsyncModalRef = {
  showModal: () => Promise<boolean>;
};

export const AsyncModal = React.forwardRef<AsyncModalRef, AsyncModalProps>(
  ({ title, content, confirmText = "Confirm", cancelText = "Cancel" }, ref) => {
    const [isOpen, setIsOpen] = useState(false);
    const [resolver, setResolver] = useState<((value: boolean) => void) | null>(
      null
    );

    const showModal = useCallback(() => {
      setIsOpen(true);
      return new Promise<boolean>((resolve) => {
        setResolver(() => resolve);
      });
    }, []);

    const handleConfirm = useCallback(() => {
      setIsOpen(false);
      if (resolver) resolver(true);
    }, [resolver]);

    const handleCancel = useCallback(() => {
      setIsOpen(false);
      if (resolver) resolver(false);
    }, [resolver]);

    React.useImperativeHandle(ref, () => ({
      showModal,
    }));

    return (
      <Modal
        title={title}
        isOpen={isOpen}
        onOpenChange={setIsOpen}
        width="min-w-3xl"
      >
        <>
          <div className="p-10">{content}</div>
          <div >
            <Button  onClick={handleCancel}>
              {cancelText}
            </Button>
            <Button  onClick={handleConfirm}>
              {confirmText}
            </Button>
          </div>
        </>
      </Modal>
    );
  }
);

AsyncModal.displayName = "AsyncModal";

type CreateAsyncModalResult = [React.FC, () => Promise<boolean>];

export const useAsyncModal = (
  modalProps: AsyncModalProps
): CreateAsyncModalResult => {
  const modalRef = React.createRef<AsyncModalRef>();

  const showModal = () => {
    if (modalRef.current) {
      return modalRef.current.showModal();
    }
    throw new Error("Modal component not initialized");
  };

  const ModalWrapper: React.FC = () => {
    return <AsyncModal {...modalProps} ref={modalRef} />;
  };

  return [ModalWrapper, showModal];
};


I like this pattern, it allows me to show some UI conditionally typically during a validation process. Kind of like window.confirm, but with my own UI.

I am using it in my app here:

function CampaignEditor() {
//...un important code
  const [OptOutWarningModal, showOptOutWarningModal] = useAsyncModal({
    title: "Opt out warning",
    content:
      "You have not provided instructions to opt out. Are you sure you want to continue?",
    confirmText: "Yes",
    cancelText: "No",
  });

  const {
    control,
    handleSubmit,
    setValue,
    watch,
    formState: { errors },
  } = useForm<CampaignFormData>({
    resolver: zodResolver(campaignSchema),
    defaultValues: {
      ...initialCampaign,
      Status: initialCampaign.Status || "",
    },
  });

  const onSubmit = async (data: CampaignFormData) => {
//...unimportant code

    // Warn user if they have not included an opt out instruction
    if (!data.Message.toLowerCase().includes("to opt out reply stop")) {
      const optOutWarning = await showOptOutWarningModal();
      if (!optOutWarning) return;
    }

   //... more not important logic
  };


Finally, I mount my component:

  return (
    <div >
      <h2>New campaign</h2>
      <form
        onSubmit={handleSubmit(onSubmit)}
      >
        <OptOutWarningModal />
       //.... my form
          <Button onClick={() => setCloseForm(true)} type="submit">
            Save & Close
          </Button>
          <Button type="submit">Save</Button>
          <Button onClick={() => navigate({ to: "/" })}>Cancel</Button>
        </div>
      </form>
    </div>
  );
Was this page helpful?