# 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 ```typescript // Track which beneficiary is being loaded const loadingBeneficiaryIdRef = useRef(null); const abortControllerRef = useRef(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