fix: cancel stale debounce on blur in OneLineEditor to prevent race condition

When the user blurs a field immediately after typing (e.g. clicking Add Row),
the 100ms debounce could fire after the subsequent write, overwriting the DB
with stale kvPairs that didn't include the new row. Fix by: adding cancel() to
misc.debounce, merging changes/blur into a single effect registered once via a
ref (eliminating stale closures), and having blur explicitly cancel any pending
debounce before flushing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
jackkav
2026-05-26 13:33:21 +02:00
committed by Jack Kavanagh
parent cd9ddb07ed
commit 22a9c7995e
2 changed files with 17 additions and 11 deletions

View File

@@ -109,6 +109,7 @@ export const debounce = <F extends (...args: Parameters<F>) => ReturnType<F>>(
clearTimeout(timeout);
timeout = setTimeout(() => func(...args), waitFor);
};
debounced.cancel = () => clearTimeout(timeout);
return debounced;
};

View File

@@ -65,6 +65,8 @@ export const OneLineEditor = forwardRef<OneLineEditorHandle, OneLineEditorProps>
) => {
const textAreaRef = useRef<HTMLTextAreaElement>(null);
const codeMirror = useRef<CodeMirror.EditorFromTextArea | null>(null);
const onChangeRef = useRef(onChange);
onChangeRef.current = onChange;
const { settings } = useRootLoaderData()!;
const { isOwner, isEnterprisePlan } = usePlanData();
const { handleRender, handleGetRenderContext } = useNunjucks();
@@ -296,23 +298,26 @@ export const OneLineEditor = forwardRef<OneLineEditorHandle, OneLineEditorProps>
useEffect(() => {
const fn = misc.debounce((doc: CodeMirror.Editor) => {
if (onChange) {
onChange(doc.getValue() || '');
if (onChangeRef.current) {
onChangeRef.current(doc.getValue() || '');
}
}, DEBOUNCE_MILLIS);
codeMirror.current?.on('changes', fn);
return () => codeMirror.current?.off('changes', fn);
}, [onChange]);
useEffect(() => {
const flushOnBlur = (doc: CodeMirror.Editor) => {
if (onChange) {
onChange(doc.getValue() || '');
// Cancel the pending debounce so a stale fire can't overwrite state after blur
fn.cancel();
if (onChangeRef.current) {
onChangeRef.current(doc.getValue() || '');
}
};
codeMirror.current?.on('changes', fn);
codeMirror.current?.on('blur', flushOnBlur);
return () => codeMirror.current?.off('blur', flushOnBlur);
}, [onChange]);
return () => {
fn.cancel();
codeMirror.current?.off('changes', fn);
codeMirror.current?.off('blur', flushOnBlur);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
const unsubscribe = window.main.on(