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];
};
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
};
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>
);
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>
);
2 Replies
Cody
CodyOP3mo ago
Now, when I submit my form, and input is valid, my onsubmit function runs and I get to this line
const optOutWarning = await showOptOutWarningModal();
const optOutWarning = await showOptOutWarningModal();
And I am getting an error "Modal component not initialized" Which is super weird. I am clearly rendering this modal in my jsx, its not conditional at all. Whats REALLY weird, is if just throw a button on the page with an onclick of showOptOutWarningModal(), it works perfectly. It seems to be not behaving correctly from inside the onSubmit function. I have tried making that function a useCallback with dependencies, no help. I am a bit lost on how to continue debugging this. Any input from some React wizards would be so helpful. Thankyou!
bakdaddy
bakdaddy3mo ago
hi @Cody , I tried to reproduce your code, and it is weirdly working for me, though I skipped the useForm hook and had my own implementation of Modal element. here's the link to the codesandbox, you can check it out https://codesandbox.io/p/sandbox/g2k8gl maybe the issue is in the different part of the app that you think I don't know if you've already figured it out or no (judging from the time you've first posted), but let me know if there's any more things poping up
Want results from more Discord servers?
Add your server