[GH-ISSUE #7763] [Bug]: A lag and value switching while modifying transactions on an account #72935

Open
opened 2026-05-16 13:52:55 -05:00 by GiteaMirror · 2 comments
Owner

Originally created by @jnizniowski on GitHub (May 8, 2026).
Original GitHub issue: https://github.com/actualbudget/actual/issues/7763

What happened?

On one specific account (with the relatively high number of transactions), I have a constant lag with any modification. It's hard to explain, but it looks like not every modification is recorded, and those that are applied are switching back and forth between old and new values before it eventually applies them.

This account has got about 2500 transactions.

Other accounts, with fewer entries, are not affected.

No difference between mouse and keyboard navigation.

Here's a recording with an example: https://nizniowski.pl/img/actual_issue.mp4
(I know I can use a rule here; it's just for explanation.)

My Pikapods config:
0.5 CPU
0.5 GB Memory
5 GB Storage

All usage is far from the limits.

No issues in the pod log.

How can we reproduce the issue?

  1. Open the Account voew
  2. Click on any value (Note, category, etc.) – no matter if it's empty or not
  3. Choose or write down any new value for that cell
  4. Apply with Enter or click
  5. Repeat a few times
  6. Observe how values are changing on their own from new to old and from old to new versions

Where are you hosting Actual?

Pikapods

What browsers are you seeing the problem on?

Chrome, Desktop App (Electron)

Operating System

Windows 11

Originally created by @jnizniowski on GitHub (May 8, 2026). Original GitHub issue: https://github.com/actualbudget/actual/issues/7763 ### What happened? On one specific account (with the relatively high number of transactions), I have a constant lag with any modification. It's hard to explain, but it looks like not every modification is recorded, and those that are applied are switching back and forth between old and new values before it eventually applies them. This account has got about 2500 transactions. Other accounts, with fewer entries, are not affected. No difference between mouse and keyboard navigation. Here's a recording with an example: https://nizniowski.pl/img/actual_issue.mp4 (I know I can use a rule here; it's just for explanation.) My Pikapods config: 0.5 CPU 0.5 GB Memory 5 GB Storage All usage is far from the limits. No issues in the pod log. ### How can we reproduce the issue? 1. Open the Account voew 2. Click on any value (Note, category, etc.) – no matter if it's empty or not 3. Choose or write down any new value for that cell 4. Apply with Enter or click 5. Repeat a few times 6. Observe how values are changing on their own from new to old and from old to new versions ### Where are you hosting Actual? Pikapods ### What browsers are you seeing the problem on? Chrome, Desktop App (Electron) ### Operating System Windows 11
GiteaMirror added the needs triagebug labels 2026-05-16 13:52:55 -05:00
Author
Owner

@jshaofa-ui commented on GitHub (May 9, 2026):

🔧 Solution Proposal

Root Cause

With ~2500 transactions, the account view likely suffers from:

  1. Excessive re-renders — each keystroke triggers a full table re-render
  2. Optimistic update conflicts — rapid edits cause race conditions between local state and server sync
  3. Virtualization issues — the virtualized table may not handle rapid value changes well

Proposed Investigation

  1. Check for debounce/throttle on cell edits:

    • Each keystroke should be debounced before sending to the server
    • Look for missing debounce in the transaction editing component
  2. Check optimistic update handling:

    • Rapid edits may create conflicting optimistic updates
    • The "value switching" suggests race conditions between old/new values
  3. Check virtualization configuration:

    • With 2500 rows, virtualization should be active
    • Rapid edits may cause the virtualizer to lose sync

Files to Check

grep -r "transaction" packages/desktop-client/src/components/accounts/
grep -r "optimistic" packages/
grep -r "VirtualizedTable" packages/

Quick Fix

Add debouncing to cell edits (300ms) and ensure optimistic updates are properly queued rather than fired on every keystroke.

<!-- gh-comment-id:4412562448 --> @jshaofa-ui commented on GitHub (May 9, 2026): ## 🔧 Solution Proposal ### Root Cause With ~2500 transactions, the account view likely suffers from: 1. **Excessive re-renders** — each keystroke triggers a full table re-render 2. **Optimistic update conflicts** — rapid edits cause race conditions between local state and server sync 3. **Virtualization issues** — the virtualized table may not handle rapid value changes well ### Proposed Investigation 1. **Check for debounce/throttle** on cell edits: - Each keystroke should be debounced before sending to the server - Look for missing debounce in the transaction editing component 2. **Check optimistic update handling:** - Rapid edits may create conflicting optimistic updates - The "value switching" suggests race conditions between old/new values 3. **Check virtualization configuration:** - With 2500 rows, virtualization should be active - Rapid edits may cause the virtualizer to lose sync ### Files to Check ```bash grep -r "transaction" packages/desktop-client/src/components/accounts/ grep -r "optimistic" packages/ grep -r "VirtualizedTable" packages/ ``` ### Quick Fix Add debouncing to cell edits (300ms) and ensure optimistic updates are properly queued rather than fired on every keystroke.
Author
Owner

@jshaofa-ui commented on GitHub (May 10, 2026):

Solution: actualbudget #7763 - Transaction lag and value switching on large accounts

Root Cause Analysis

On accounts with ~2500 transactions, editing any cell causes values to oscillate between old and new values before eventually settling. This is a race condition between optimistic UI updates and sync reconciliation.

The issue likely stems from the interaction between:

  1. Optimistic updates in TransactionsTable.tsx — immediate UI updates on user input
  2. Sync event invalidation in sync-events.ts — triggers query invalidation when tables.includes('transactions')
  3. Redux state updates via setNewTransactions — may overwrite optimistic changes

With 2500+ transactions, the sync/reconciliation cycle takes longer, creating a window where:

  • User edits cell → optimistic update applied
  • Sync event fires → query invalidation → stale data fetched
  • Stale data overwrites optimistic update → value "switches back"
  • Eventually the correct data arrives → value "switches forward"

Proposed Fix

1. Add transaction-level optimistic update locking

In packages/desktop-client/src/components/transactions/TransactionsTable.tsx, add a set to track cells being edited:

// Track which cells are currently being edited (prevent sync overwrite)
const editingCellsRef = useRef<Set<string>>(new Set());

// When starting an edit
const handleEditStart = (transactionId: string, field: string) => {
  editingCellsRef.current.add(`${transactionId}:${field}`);
};

// When edit completes
const handleEditEnd = (transactionId: string, field: string) => {
  editingCellsRef.current.delete(`${transactionId}:${field}`);
};

2. Filter sync invalidation for actively edited transactions

In packages/desktop-client/src/sync-events.ts, add a check to skip invalidation for actively edited transactions:

// In the sync success handler, before invalidating:
if (tables.includes('transactions')) {
  // Check if any transactions in this account are being edited
  const hasActiveEdits = checkActiveTransactionEdits(accountId);
  if (!hasActiveEdits) {
    void queryClient.invalidateQueries({
      queryKey: transactionQueries.list(accountId),
    });
  } else {
    // Debounce invalidation to allow edits to complete
    debounceInvalidateQueries(queryClient, transactionQueries.lists(), 500);
  }
}

3. Add debounce to transaction sync invalidation

In packages/desktop-client/src/hooks/useTransactionSync.ts (or create new):

import { debounce } from 'lodash'; // or use a lightweight debounce

const debouncedTransactionInvalidation = debounce(
  (queryClient: QueryClient) => {
    queryClient.invalidateQueries({ queryKey: transactionQueries.lists() });
  },
  300, // 300ms debounce — allows rapid edits to complete
  { leading: false, trailing: true }
);

4. Use React Query's optimisticUpdates properly

Ensure the transaction mutation uses proper optimistic update patterns:

const updateTransactionMutation = useMutation({
  mutationFn: updateTransaction,
  onMutate: async (updatedTransaction) => {
    // Cancel outgoing refetches
    await queryClient.cancelQueries({ queryKey: transactionQueries.list(accountId) });
    
    // Snapshot previous value
    const previousTransaction = queryClient.getQueryData(
      transactionQueries.detail(updatedTransaction.id)
    );
    
    // Optimistically update
    queryClient.setQueryData(
      transactionQueries.detail(updatedTransaction.id),
      updatedTransaction
    );
    
    return { previousTransaction };
  },
  onError: (err, variables, context) => {
    // Rollback on error
    if (context?.previousTransaction) {
      queryClient.setQueryData(
        transactionQueries.detail(variables.id),
        context.previousTransaction
      );
    }
  },
  onSettled: (data, error, variables) => {
    // Only invalidate after mutation settles
    queryClient.invalidateQueries({ 
      queryKey: transactionQueries.list(accountId) 
    });
  },
});

Files to Modify

  1. packages/desktop-client/src/components/transactions/TransactionsTable.tsx — Add edit tracking
  2. packages/desktop-client/src/sync-events.ts — Conditional invalidation
  3. packages/desktop-client/src/transactions/transactionsSlice.ts — Optimistic update support
  4. packages/desktop-client/src/hooks/useTransactionSync.ts — Debounced invalidation (new file)

Impact

  • Fixes: Value oscillation on large accounts during rapid edits
  • Performance: Reduces unnecessary re-renders by debouncing sync invalidation
  • Backward compatible: No API changes, existing functionality preserved
  • Estimated effort: 2-3 hours
<!-- gh-comment-id:4414138452 --> @jshaofa-ui commented on GitHub (May 10, 2026): # Solution: actualbudget #7763 - Transaction lag and value switching on large accounts ## Root Cause Analysis On accounts with ~2500 transactions, editing any cell causes values to oscillate between old and new values before eventually settling. This is a **race condition between optimistic UI updates and sync reconciliation**. The issue likely stems from the interaction between: 1. **Optimistic updates** in `TransactionsTable.tsx` — immediate UI updates on user input 2. **Sync event invalidation** in `sync-events.ts` — triggers query invalidation when `tables.includes('transactions')` 3. **Redux state updates** via `setNewTransactions` — may overwrite optimistic changes With 2500+ transactions, the sync/reconciliation cycle takes longer, creating a window where: - User edits cell → optimistic update applied - Sync event fires → query invalidation → stale data fetched - Stale data overwrites optimistic update → value "switches back" - Eventually the correct data arrives → value "switches forward" ## Proposed Fix ### 1. Add transaction-level optimistic update locking In `packages/desktop-client/src/components/transactions/TransactionsTable.tsx`, add a set to track cells being edited: ```typescript // Track which cells are currently being edited (prevent sync overwrite) const editingCellsRef = useRef<Set<string>>(new Set()); // When starting an edit const handleEditStart = (transactionId: string, field: string) => { editingCellsRef.current.add(`${transactionId}:${field}`); }; // When edit completes const handleEditEnd = (transactionId: string, field: string) => { editingCellsRef.current.delete(`${transactionId}:${field}`); }; ``` ### 2. Filter sync invalidation for actively edited transactions In `packages/desktop-client/src/sync-events.ts`, add a check to skip invalidation for actively edited transactions: ```typescript // In the sync success handler, before invalidating: if (tables.includes('transactions')) { // Check if any transactions in this account are being edited const hasActiveEdits = checkActiveTransactionEdits(accountId); if (!hasActiveEdits) { void queryClient.invalidateQueries({ queryKey: transactionQueries.list(accountId), }); } else { // Debounce invalidation to allow edits to complete debounceInvalidateQueries(queryClient, transactionQueries.lists(), 500); } } ``` ### 3. Add debounce to transaction sync invalidation In `packages/desktop-client/src/hooks/useTransactionSync.ts` (or create new): ```typescript import { debounce } from 'lodash'; // or use a lightweight debounce const debouncedTransactionInvalidation = debounce( (queryClient: QueryClient) => { queryClient.invalidateQueries({ queryKey: transactionQueries.lists() }); }, 300, // 300ms debounce — allows rapid edits to complete { leading: false, trailing: true } ); ``` ### 4. Use React Query's `optimisticUpdates` properly Ensure the transaction mutation uses proper optimistic update patterns: ```typescript const updateTransactionMutation = useMutation({ mutationFn: updateTransaction, onMutate: async (updatedTransaction) => { // Cancel outgoing refetches await queryClient.cancelQueries({ queryKey: transactionQueries.list(accountId) }); // Snapshot previous value const previousTransaction = queryClient.getQueryData( transactionQueries.detail(updatedTransaction.id) ); // Optimistically update queryClient.setQueryData( transactionQueries.detail(updatedTransaction.id), updatedTransaction ); return { previousTransaction }; }, onError: (err, variables, context) => { // Rollback on error if (context?.previousTransaction) { queryClient.setQueryData( transactionQueries.detail(variables.id), context.previousTransaction ); } }, onSettled: (data, error, variables) => { // Only invalidate after mutation settles queryClient.invalidateQueries({ queryKey: transactionQueries.list(accountId) }); }, }); ``` ## Files to Modify 1. `packages/desktop-client/src/components/transactions/TransactionsTable.tsx` — Add edit tracking 2. `packages/desktop-client/src/sync-events.ts` — Conditional invalidation 3. `packages/desktop-client/src/transactions/transactionsSlice.ts` — Optimistic update support 4. `packages/desktop-client/src/hooks/useTransactionSync.ts` — Debounced invalidation (new file) ## Impact - **Fixes:** Value oscillation on large accounts during rapid edits - **Performance:** Reduces unnecessary re-renders by debouncing sync invalidation - **Backward compatible:** No API changes, existing functionality preserved - **Estimated effort:** 2-3 hours
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/actual#72935