From 22a9c7995ebcf53288752e35a511fda281b29ef9 Mon Sep 17 00:00:00 2001 From: jackkav Date: Tue, 26 May 2026 13:33:21 +0200 Subject: [PATCH] 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 --- packages/insomnia/src/common/misc.ts | 1 + .../.client/codemirror/one-line-editor.tsx | 27 +++++++++++-------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/packages/insomnia/src/common/misc.ts b/packages/insomnia/src/common/misc.ts index dbd255c9c2..111760e4a5 100644 --- a/packages/insomnia/src/common/misc.ts +++ b/packages/insomnia/src/common/misc.ts @@ -109,6 +109,7 @@ export const debounce = ) => ReturnType>( clearTimeout(timeout); timeout = setTimeout(() => func(...args), waitFor); }; + debounced.cancel = () => clearTimeout(timeout); return debounced; }; diff --git a/packages/insomnia/src/ui/components/.client/codemirror/one-line-editor.tsx b/packages/insomnia/src/ui/components/.client/codemirror/one-line-editor.tsx index 0a30a2c3e3..d5e186a792 100644 --- a/packages/insomnia/src/ui/components/.client/codemirror/one-line-editor.tsx +++ b/packages/insomnia/src/ui/components/.client/codemirror/one-line-editor.tsx @@ -65,6 +65,8 @@ export const OneLineEditor = forwardRef ) => { const textAreaRef = useRef(null); const codeMirror = useRef(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 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(