WellNuo/docs/RACE_CONDITION_FIX.md
Sergei f6ba2a906a Fix race conditions when quickly switching beneficiaries
Implemented request tracking and cancellation to prevent stale API
responses from overwriting current beneficiary data.

Changes:
- Added loadingBeneficiaryIdRef to track which beneficiary is being loaded
- Added AbortController to cancel in-flight requests
- Validate beneficiary ID before applying state updates
- Cleanup on component unmount to prevent memory leaks

This fixes the issue where rapidly switching between beneficiaries
would show wrong data if slower requests completed after faster ones.

🤖 Generated with Claude Code (https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-29 12:33:57 -08:00

4.2 KiB

Race Condition Fix - Beneficiary Switching

Problem

When quickly switching between beneficiaries, multiple API requests could be in flight simultaneously. The last response to arrive would win, even if it wasn't for the currently selected beneficiary. This caused:

  1. Wrong beneficiary data displayed
  2. Context set to stale beneficiary
  3. Confusing UI state

Example Scenario

User selects Beneficiary 1 → API request starts (slow, 200ms)
User quickly selects Beneficiary 2 → API request starts (fast, 50ms)

Timeline:
t=0ms:   Request for Beneficiary 1 starts
t=10ms:  Request for Beneficiary 2 starts
t=60ms:  Response for Beneficiary 2 arrives → UI shows Beneficiary 2 ✅
t=210ms: Response for Beneficiary 1 arrives → UI incorrectly shows Beneficiary 1 ❌

Problem: The slower request overwrites the correct beneficiary!

Solution

Implemented a tracking mechanism using React refs to ignore stale API responses:

Key Changes

  1. Track current loading ID (loadingBeneficiaryIdRef)

    • Stores which beneficiary is currently being loaded
    • Updated whenever a new load starts
    • Checked before applying any state updates
  2. AbortController for cancellation (abortControllerRef)

    • Cancels previous in-flight requests when a new one starts
    • Cleaned up on component unmount
    • Prevents memory leaks
  3. Validation before state updates

    • Every API response checks if it's still current
    • Stale responses are silently ignored
    • Loading states only update for current requests

Implementation Details

// Track which beneficiary is being loaded
const loadingBeneficiaryIdRef = useRef<string | null>(null);
const abortControllerRef = useRef<AbortController | null>(null);

const loadBeneficiary = useCallback(async (showLoadingIndicator = true) => {
  // Cancel previous request
  if (abortControllerRef.current) {
    abortControllerRef.current.abort();
  }

  // Create new controller
  const abortController = new AbortController();
  abortControllerRef.current = abortController;

  // Track current ID
  const currentLoadingId = id;
  loadingBeneficiaryIdRef.current = currentLoadingId;

  try {
    const response = await api.getWellNuoBeneficiary(parseInt(id, 10));

    // Check if still current before updating state
    if (abortController.signal.aborted ||
        loadingBeneficiaryIdRef.current !== currentLoadingId) {
      return; // Ignore stale response
    }

    // Safe to update state - this is the current beneficiary
    setBeneficiary(data);
    setCurrentBeneficiary(data);
  } catch (err) {
    // Only update error if still current
    if (!abortController.signal.aborted &&
        loadingBeneficiaryIdRef.current === currentLoadingId) {
      setError(err.message);
    }
  }
}, [id, setCurrentBeneficiary]);

// Cleanup on unmount or ID change
useEffect(() => {
  loadBeneficiary();

  return () => {
    if (abortControllerRef.current) {
      abortControllerRef.current.abort();
    }
    loadingBeneficiaryIdRef.current = null;
  };
}, [loadBeneficiary]);

Testing

Created unit tests (__tests__/utils/beneficiary-race-condition-prevention.test.ts) that verify:

  1. Stale API responses are ignored
  2. AbortController cancels previous requests
  3. Rapid switching handles out-of-order responses correctly
  4. Error responses don't cause race conditions
  5. Reloading same beneficiary still works

Files Modified

  • app/(tabs)/beneficiaries/[id]/index.tsx - Added race condition prevention logic
  • __tests__/utils/beneficiary-race-condition-prevention.test.ts - Unit tests for the fix

Verified

  • TypeScript compilation passes
  • No new type errors introduced
  • Logic handles all edge cases (errors, unmount, same ID reload)

How to Test Manually

  1. Open the app with multiple beneficiaries
  2. Quickly tap between different beneficiaries in the list
  3. Verify that the displayed beneficiary always matches the one you selected
  4. Check that no flickering or wrong data appears

Future Improvements

  • Consider adding a visual indicator when switching beneficiaries
  • Add analytics to track how often users switch beneficiaries
  • Implement optimistic UI updates with data prefetching