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>
4.2 KiB
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:
- Wrong beneficiary data displayed
- Context set to stale beneficiary
- 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
-
Track current loading ID (
loadingBeneficiaryIdRef)- Stores which beneficiary is currently being loaded
- Updated whenever a new load starts
- Checked before applying any state updates
-
AbortController for cancellation (
abortControllerRef)- Cancels previous in-flight requests when a new one starts
- Cleaned up on component unmount
- Prevents memory leaks
-
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:
- ✅ Stale API responses are ignored
- ✅ AbortController cancels previous requests
- ✅ Rapid switching handles out-of-order responses correctly
- ✅ Error responses don't cause race conditions
- ✅ 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
- Open the app with multiple beneficiaries
- Quickly tap between different beneficiaries in the list
- Verify that the displayed beneficiary always matches the one you selected
- 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