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:
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:
Finally, I mount my component:
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>
);