Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 0 additions & 10 deletions src/features/listing-builder/atoms/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,6 @@ const descriptionKeyAtom = atom<string | number>(1);
const skillsKeyAtom = atom<string | number>(1);
const isListingInReviewAtom = atom<boolean>(false);

interface SaveQueueState {
isProcessing: boolean;
shouldProcessNext: boolean;
}
const draftQueueAtom = atom<SaveQueueState>({
isProcessing: false,
shouldProcessNext: false,
});

const confirmModalAtom = atom<'SUCCESS' | 'VERIFICATION' | undefined>(
undefined,
);
Expand Down Expand Up @@ -62,7 +53,6 @@ const submitListingMutationAtom = atomWithMutation((get) => ({
export {
confirmModalAtom,
descriptionKeyAtom,
draftQueueAtom,
hackathonsAtom,
hideAutoSaveAtom,
isDraftSavingAtom,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import { Header } from '@/features/navbar/components/Header';

import {
confirmModalAtom,
draftQueueAtom,
hackathonsAtom,
isEditingAtom,
isGodAtom,
Expand Down Expand Up @@ -187,13 +186,6 @@ function ListingBuilderProvider({
isListingInReviewAtom,
isEditing && listing && dayjs().isAfter(listing.deadline),
],
[
draftQueueAtom,
{
isProcessing: false,
shouldProcessNext: false,
},
],
]}
>
<ListingEditor
Expand Down
72 changes: 20 additions & 52 deletions src/features/listing-builder/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { dayjs } from '@/utils/dayjs';

import {
descriptionKeyAtom,
draftQueueAtom,
hackathonsAtom,
hideAutoSaveAtom,
isDraftSavingAtom,
Expand All @@ -30,6 +29,13 @@ import {
} from '../types/schema';
import { getListingDefaults, refineReadyListing } from '../utils/form';

const formatDraftData = (data: Partial<ListingFormData>) => {
if (data.deadline) {
if (!data.deadline.endsWith('Z')) data.deadline += dayjs().format('Z');
}
return data;
};

interface UseListingFormReturn extends UseFormReturn<ListingFormData> {
saveDraft: () => void;
submitListing: () => Promise<ListingFormData>;
Expand Down Expand Up @@ -87,62 +93,22 @@ export const useListingForm = (
const saveDraftMutation = useAtomValue(saveDraftMutationAtom);
const submitListingMutation = useAtomValue(submitListingMutationAtom);
const [, setDraftSaving] = useAtom(isDraftSavingAtom);

const [queueRef, setQueueRef] = useAtom(draftQueueAtom);

const [, setHideAutoSave] = useAtom(hideAutoSaveAtom);
const queueRefRef = useRef(queueRef);

useEffect(() => {
queueRefRef.current = queueRef;
}, [queueRef]);

// queue ensures eeach call for auto save is sent synchronously
const processSaveQueue = useCallback(async () => {
const saveDraft = useCallback(async () => {
if (isEditing) return;
setDraftSaving(true);
if (queueRefRef.current.isProcessing) {
setQueueRef((q) => ({
...q,
shouldProcessNext: true,
}));
return;
}

setQueueRef((q) => ({
...q,
shouldProcessNext: false,
isProcessing: true,
}));
try {
const dataToSave = getValues();

if (dataToSave.deadline) {
if (!dataToSave.deadline.endsWith('Z'))
dataToSave.deadline += dayjs().format('Z');
}
const listingData = getValues();
const dataToSave = formatDraftData(listingData);
const data = await saveDraftMutation.mutateAsync(dataToSave);
setHideAutoSave(false);
formMethods.setValue('id', data.id);
if (!dataToSave.slug) formMethods.setValue('slug', data.slug);
setQueueRef((q) => ({
...q,
}));
} catch (error) {
console.log('Error processSaveQueue', error);
console.log('Error saving draft', error);
} finally {
setDraftSaving(false);
setQueueRef((q) => ({
...q,
isProcessing: false,
}));
// Check if we need to process another save
if (queueRefRef.current.shouldProcessNext) {
// Use setTimeout to break the call stack and ensure queue state is updated
setTimeout(() => {
void processSaveQueue();
}, 0);
}
}
}, [
getValues,
Expand All @@ -153,19 +119,21 @@ export const useListingForm = (
isEditing,
]);

const debouncedSaveRef = useRef<ReturnType<typeof debounce>>(undefined);
const latestSaveDraftRef = useRef<() => void>(saveDraft);
useEffect(() => {
latestSaveDraftRef.current = saveDraft;
}, [saveDraft]);

const debouncedRef = useRef<ReturnType<typeof debounce> | null>(null);
useEffect(() => {
debouncedSaveRef.current = debounce(() => {
void processSaveQueue();
}, 1000);
}, [processSaveQueue]);
debouncedRef.current = debounce(latestSaveDraftRef.current, 1000);
return () => debouncedRef.current?.cancel();
}, []);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Stale Function Breaks Debounce Updates

The debounced function captures the initial saveDraft function value instead of dynamically calling the latest version. When saveDraft recreates due to dependency changes (like isEditing, getValues, or saveDraftMutation), the debounced function continues calling the stale version with outdated closures. This breaks the debounce functionality as it won't use updated values. The pattern should wrap the ref call: debounce(() => latestSaveDraftRef.current(), 1000).

Fix in Cursor Fix in Web


const onChange = useCallback(() => {
setHideAutoSave(true);
if (!isEditing) {
debouncedSaveRef.current?.cancel();
debouncedSaveRef.current?.();
debouncedRef.current?.();
}
}, [isEditing]);

Expand Down