Skip to content

fix - not saving editor state to convex when closing/ moving to other… - #22

Open
tarunislucky wants to merge 3 commits into
code-with-antonio:mainfrom
tarunislucky:fix-bug-ediotor-debounce
Open

fix - not saving editor state to convex when closing/ moving to other…#22
tarunislucky wants to merge 3 commits into
code-with-antonio:mainfrom
tarunislucky:fix-bug-ediotor-debounce

Conversation

@tarunislucky

@tarunislucky tarunislucky commented May 4, 2026

Copy link
Copy Markdown

Issue

When a user edits a file and switches to another file before the debounce timeout (1500ms) completes, the pending changes are lost without being saved to Convex.

Root Cause

The useEffect cleanup function was clearing the debounce timer when activeTabId changed, but it wasn't flushing the pending content before clearing the timeout. This resulted in unsaved edits being discarded.

Steps to Reproduce

  • Open a file in the editor
  • Type some text
  • Immediately switch to another file (before 1.5 seconds pass)
  • Observe: Changes are lost and not saved to Convex

Solution

  • Added pendingContentRef to track unsaved content changes
  • Modified the cleanup function to flush pending changes to Convex before clearing the timeout
  • Ensures all edits are persisted when switching files or unmounting the component

Changes

  • Track pending content in a ref while debouncing
  • Save pending content before clearing the timer in useEffect cleanup
  • Clear the pending content ref after successfully updating
  • This prevents accidental data loss while maintaining the debounce optimization for database updates.

Summary by CodeRabbit

  • Bug Fixes
    • Improved editor auto-save: edits are now debounced to reduce redundant saves while typing, and any unsaved edits are committed immediately when switching tabs or closing the editor.
    • This ensures recent changes are preserved reliably and reduces unexpected lost work during normal use.

@vercel

vercel Bot commented May 4, 2026

Copy link
Copy Markdown

@tarunislucky is attempting to deploy a commit to the Jason's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented May 4, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

EditorView now tracks the latest unsaved editor content in pendingContentRef. Each editor change updates the ref and schedules a debounced updateFile; on unmount or active-tab change the cleanup immediately persists any pending content before clearing the ref.

Changes

Content Persistence Enhancement

Layer / File(s) Summary
Ref Initialization
src/features/editor/components/editor-view.tsx
Introduces pendingContentRef as useRef<string | null> to hold the most recent unsaved editor content.
Editor Change Handler
src/features/editor/components/editor-view.tsx
CodeEditor onChange writes each new content into pendingContentRef, clears existing debounce timeout, and schedules a debounced updateFile that persists the scheduled content and clears the ref.
Effect Cleanup & Persistence
src/features/editor/components/editor-view.tsx
Effect cleanup now, on unmount or activeTabId change, calls updateFile with pendingContentRef.current (when present) and clears the ref, replacing prior behavior that only cleared the debounce timeout.

Sequence Diagram

sequenceDiagram
    participant User
    participant CodeEditor
    participant EditorView
    participant Persistence
    User->>CodeEditor: Type / update text
    CodeEditor->>EditorView: onChange(newContent)
    EditorView-->>EditorView: pendingContentRef = newContent\nclear existing timeout\nset debounce timeout
    par Debounce fires later
        EditorView->>Persistence: updateFile(pendingContentRef)
        EditorView-->>EditorView: pendingContentRef = null
    end
    alt Unmount or active tab change before debounce
        EditorView->>Persistence: updateFile(pendingContentRef) (immediate)
        EditorView-->>EditorView: pendingContentRef = null
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰✨ I nibble keys as lines accrue,
A quiet ref to guard what's new—
If tabs do close or timeouts race,
Your words find home, no trace misplaced.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main bug being fixed: unsaved editor state not persisting when closing or switching files.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/features/editor/components/editor-view.tsx (1)

26-36: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Cleanup effect has dependency and error-handling concerns.

Two issues with this cleanup:

  1. activeFile in dependency array may defeat debouncing: When updateFile succeeds, Convex will push an updated document, causing useFile to return a new object reference. This triggers the effect cleanup, which could flush pending content prematurely if the user is still typing. Consider using only activeTabId (which is stable) and storing the ID in a ref for the cleanup.

  2. Fire-and-forget mutation risks silent data loss: The updateFile call has no error handling. Per the relevant snippet, this mutation can throw on auth failures or network issues. During unmount or navigation, there's no opportunity to retry or inform the user.

Proposed fix
 const timeoutRef = useRef<NodeJS.Timeout | null>(null);
 const pendingContentRef = useRef<string | null>(null);
+const activeFileIdRef = useRef<Id<"files"> | null>(null);

+// Keep activeFileIdRef in sync
+useEffect(() => {
+  activeFileIdRef.current = activeFile?._id ?? null;
+}, [activeFile?._id]);

 // Cleanup pending debounced updates on unmount or file change
 useEffect(() => {
   return () => {
     if (timeoutRef.current) {
       clearTimeout(timeoutRef.current);
     }
-    if (pendingContentRef.current && activeFile) {
-      updateFile({ id: activeFile._id, content: pendingContentRef.current });
+    if (pendingContentRef.current && activeFileIdRef.current) {
+      updateFile({ id: activeFileIdRef.current, content: pendingContentRef.current })
+        .catch((err) => console.error("Failed to save pending changes:", err));
       pendingContentRef.current = null;
     }
   };
-}, [activeTabId, activeFile, updateFile]);
+}, [activeTabId, updateFile]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/editor/components/editor-view.tsx` around lines 26 - 36, The
cleanup effect should stop depending on the changing activeFile object and
handle updateFile failures: remove activeFile from the dependency array (leave
activeTabId and updateFile), capture the current file id into a stable ref
(e.g., activeFileIdRef.current = activeFile?._id inside the effect when
activeFile changes), and in the cleanup use that ref (not the activeFile object)
to flush pendingContentRef; call updateFile({ id: activeFileIdRef.current,
content: pendingContentRef.current }) but handle errors by attaching .catch(...)
to the promise (log or surface an error/toast and/or enqueue a retry) rather
than fire-and-forget, and continue to clear pendingContentRef and timeoutRef as
before.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/features/editor/components/editor-view.tsx`:
- Around line 26-36: The cleanup effect should stop depending on the changing
activeFile object and handle updateFile failures: remove activeFile from the
dependency array (leave activeTabId and updateFile), capture the current file id
into a stable ref (e.g., activeFileIdRef.current = activeFile?._id inside the
effect when activeFile changes), and in the cleanup use that ref (not the
activeFile object) to flush pendingContentRef; call updateFile({ id:
activeFileIdRef.current, content: pendingContentRef.current }) but handle errors
by attaching .catch(...) to the promise (log or surface an error/toast and/or
enqueue a retry) rather than fire-and-forget, and continue to clear
pendingContentRef and timeoutRef as before.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 72a4e20f-0244-409e-857b-43e34a3eadf7

📥 Commits

Reviewing files that changed from the base of the PR and between 4cf8ccb and c930220.

📒 Files selected for processing (1)
  • src/features/editor/components/editor-view.tsx

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/features/editor/components/editor-view.tsx`:
- Around line 31-34: The updateFile call is fire-and-forget, which loses
pendingContentRef.current on non-retryable failures; wrap the Promise returned
by updateFile(id: activeTabId, content: pendingContentRef.current) with
.then/.catch so you only set pendingContentRef.current = null after the Promise
resolves successfully and handle errors in .catch (e.g., log or surface via the
app's notification/error handler) to allow retry or inform the user; apply the
same pattern to the debounce save handler (the debounce callback that calls
updateFile around line 71) so both spots attach .catch and only clear
pendingContentRef on success.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e653698a-6fe4-4406-9e28-7d03da66c873

📥 Commits

Reviewing files that changed from the base of the PR and between c930220 and 6fe1c3d.

📒 Files selected for processing (1)
  • src/features/editor/components/editor-view.tsx

Comment on lines +31 to +34
if (pendingContentRef.current !== null && activeTabId) {
updateFile({ id: activeTabId, content: pendingContentRef.current });
pendingContentRef.current = null;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Unhandled Promise in cleanup — silent data loss on non-retryable failure

updateFile(...) returns a Promise that is never handled. Convex React automatically retries mutations until they are confirmed to have been written to the database, and the backend ensures each mutation call executes only once, so transient network hiccups are covered. However, non-retryable failures (e.g., auth expiry, server-side validation errors thrown by the mutation) will be silently swallowed because:

  1. No .catch() is attached.
  2. pendingContentRef.current = null (line 33) clears the pending content synchronously before the Promise settles — meaning on a permanent failure, the content is gone with no retry path and no error surfaced to the user.

The same fire-and-forget pattern exists in the debounce callback (line 71), though that code predates this PR.

🛡️ Proposed fix — attach a .catch() to surface failures
-      if (pendingContentRef.current !== null && activeTabId) {
-        updateFile({ id: activeTabId, content: pendingContentRef.current });
-        pendingContentRef.current = null;
-      }
+      if (pendingContentRef.current !== null && activeTabId) {
+        updateFile({ id: activeTabId, content: pendingContentRef.current }).catch(
+          (err) => console.error("[EditorView] Failed to flush pending editor content:", err)
+        );
+        pendingContentRef.current = null;
+      }

For the debounce handler (line 71), the same .catch() pattern applies:

       timeoutRef.current = setTimeout(() => {
-        updateFile({ id: activeFile._id, content });
+        updateFile({ id: activeFile._id, content }).catch(
+          (err) => console.error("[EditorView] Debounced update failed:", err)
+        );
         pendingContentRef.current = null;
       }, DEBOUNCE_MS);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (pendingContentRef.current !== null && activeTabId) {
updateFile({ id: activeTabId, content: pendingContentRef.current });
pendingContentRef.current = null;
}
if (pendingContentRef.current !== null && activeTabId) {
updateFile({ id: activeTabId, content: pendingContentRef.current }).catch(
(err) => console.error("[EditorView] Failed to flush pending editor content:", err)
);
pendingContentRef.current = null;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/editor/components/editor-view.tsx` around lines 31 - 34, The
updateFile call is fire-and-forget, which loses pendingContentRef.current on
non-retryable failures; wrap the Promise returned by updateFile(id: activeTabId,
content: pendingContentRef.current) with .then/.catch so you only set
pendingContentRef.current = null after the Promise resolves successfully and
handle errors in .catch (e.g., log or surface via the app's notification/error
handler) to allow retry or inform the user; apply the same pattern to the
debounce save handler (the debounce callback that calls updateFile around line
71) so both spots attach .catch and only clear pendingContentRef on success.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant