| null>(null);
+ const mountedRef = useRef(false);
+ const dismissedRef = useRef(false);
+ const closedRef = useRef(false);
+ const pendingRef = useRef(false);
+ const completedRef = useRef(false);
+
+ useEffect(() => {
+ mountedRef.current = true;
+ return () => { mountedRef.current = false; };
+ }, []);
+
+ // Invalidate pending work as soon as dismissal starts, including Panel's
+ // closing animation. A mutation already sent to the server cannot be undone.
+ const handleCloseStart = () => {
+ dismissedRef.current = true;
+ };
+
+ const handleClose = () => {
+ handleCloseStart();
+ if (closedRef.current) return;
+ closedRef.current = true;
+ onClose();
+ };
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
+ if (!mountedRef.current || dismissedRef.current || pendingRef.current || completedRef.current) return;
const trimmedName = name.trim();
if (!trimmedName) {
@@ -54,12 +84,16 @@ export function CreateListModal({ onClose, onListCreated }: CreateListModalProps
return;
}
+ // React state alone does not prevent two submissions in the same event turn.
+ pendingRef.current = true;
setError(null);
+ setPlanLimitHit(false);
setIsCreating(true);
haptic('medium');
try {
const listAsset = await createListAsset(trimmedName, did);
+ if (!mountedRef.current || dismissedRef.current) return;
const listId = await createList({
assetDid: listAsset.assetDid,
@@ -68,7 +102,9 @@ export function CreateListModal({ onClose, onListCreated }: CreateListModalProps
categoryId,
createdAt: Date.now(),
});
+ if (!mountedRef.current || dismissedRef.current) return;
+ completedRef.current = true;
haptic('success');
const newCount = (existingLists?.filter((l) => !isNote(l)).length ?? 0) + 1;
trackListCreated(newCount);
@@ -77,6 +113,7 @@ export function CreateListModal({ onClose, onListCreated }: CreateListModalProps
setIsCreating(false);
onListCreated?.(listId, trimmedName);
} catch (err) {
+ if (!mountedRef.current || dismissedRef.current) return;
console.error("Failed to create list:", err);
if (isPlanLimitError(err)) {
setPlanLimitHit(true);
@@ -86,15 +123,20 @@ export function CreateListModal({ onClose, onListCreated }: CreateListModalProps
}
haptic('error');
setIsCreating(false);
+ } finally {
+ pendingRef.current = false;
}
};
const handleGoToList = () => {
- if (createdListId) navigate(`/list/${createdListId}`);
+ if (!createdListId || dismissedRef.current || !mountedRef.current) return;
+ dismissedRef.current = true;
+ navigate(`/list/${createdListId}`);
};
const handleShareList = () => {
- if (!createdListId) return;
+ if (!createdListId || dismissedRef.current || !mountedRef.current) return;
+ dismissedRef.current = true;
trackInviteSent('native_share');
navigate(`/list/${createdListId}`, { state: { openShare: true } });
};
@@ -115,8 +157,7 @@ export function CreateListModal({ onClose, onListCreated }: CreateListModalProps
+ {isCreating && (
+
+ You can close this dialog. A list already being saved may still be created.
+
+ )}
+
{error && (