fix: handle Apollo v4 useLazyQuery rejection to prevent permanent loading state

In Apollo v4, useLazyQuery's execute function returns a ResultPromise
that can reject on errors (unlike v3 where it resolved with the error
in the result). This caused the first object's list view to stay in
permanent loading after page refresh, because the unhandled rejection
left isRecordTableInitialLoading and isInitializingVirtualTableDataLoading
stuck in their initial states.

Two fixes:
- Add try/catch in findManyRecordsLazy to gracefully handle execute
  rejections (matching v3 behavior of returning errors in the result)
- Add try/finally in triggerInitialRecordTableDataLoad to ensure loading
  states are always cleaned up regardless of errors

https://claude.ai/code/session_01EA7CMTUTG5zFP132yvUweT
This commit is contained in:
Claude
2026-03-13 10:41:53 +01:00
committed by Félix Malfait
parent fa8876c047
commit d73fc5c08f
3 changed files with 96 additions and 68 deletions
@@ -47,7 +47,11 @@ export const useFindDuplicateRecordsQuery = ({
}
}
`,
[objectMetadataItem, objectMetadataItems, objectPermissionsByObjectMetadataId],
[
objectMetadataItem,
objectMetadataItems,
objectPermissionsByObjectMetadataId,
],
);
return {
@@ -4,6 +4,7 @@ import { useCallback, useMemo } from 'react';
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection';
import { type ErrorLike } from '@apollo/client';
import { type RecordGqlOperationFindManyResult } from '@/object-record/graphql/types/RecordGqlOperationFindManyResult';
import { type UseFindManyRecordsParams } from '@/object-record/hooks/useFindManyRecords';
import { useFindManyRecordsQuery } from '@/object-record/hooks/useFindManyRecordsQuery';
@@ -100,7 +101,28 @@ export const useLazyFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
};
}
const result = await findManyRecords({ variables: defaultVariables });
// In Apollo v4, useLazyQuery's execute can reject (unlike v3 which
// resolved with the error in the result). We catch rejections here to
// prevent unhandled exceptions that would leave loading states stuck.
let result;
try {
result = await findManyRecords({ variables: defaultVariables });
} catch (executionError) {
handleFindManyRecordsError(executionError as ErrorLike);
store.set(hasNextPageFamilyState.atomFamily(queryIdentifier), false);
store.set(cursorFamilyState.atomFamily(queryIdentifier), '');
return {
data: null,
records: [] as T[],
totalCount: 0,
hasNextPage: false,
error: executionError as ErrorLike,
};
}
if (isDefined(result?.error)) {
handleFindManyRecordsError(result.error);
}
@@ -121,85 +121,87 @@ export const useTriggerInitialRecordTableDataLoad = () => {
store.set(isInitializingVirtualTableDataLoadingCallbackState, true);
resetTableFocuses();
try {
resetTableFocuses();
resetVirtualizedRowTreadmill();
resetVirtualizedRowTreadmill();
updateRecordTableCSSVariable(
RECORD_TABLE_VERTICAL_SCROLL_SHADOW_VISIBILITY_CSS_VARIABLE_NAME,
'hidden',
);
updateRecordTableCSSVariable(
RECORD_TABLE_HORIZONTAL_SCROLL_SHADOW_VISIBILITY_CSS_VARIABLE_NAME,
'hidden',
);
const currentRecordIds = store.get(recordIndexAllRecordIds);
let records: ObjectRecord[] | null = null;
let totalCount = 0;
if (showAuthModal) {
records = SIGN_IN_BACKGROUND_MOCK_COMPANIES;
totalCount = SIGN_IN_BACKGROUND_MOCK_COMPANIES.length;
} else {
const newRecordIdByRealIndex = new Map(
store.get(recordIdByRealIndexCallbackState),
);
const newDataLoadingStatusByRealIndex = new Map(
store.get(dataLoadingStatusByRealIndexCallbackState),
updateRecordTableCSSVariable(
RECORD_TABLE_VERTICAL_SCROLL_SHADOW_VISIBILITY_CSS_VARIABLE_NAME,
'hidden',
);
for (const [realIndex] of currentRecordIds.entries()) {
newDataLoadingStatusByRealIndex.set(realIndex, 'not-loaded');
newRecordIdByRealIndex.delete(realIndex);
updateRecordTableCSSVariable(
RECORD_TABLE_HORIZONTAL_SCROLL_SHADOW_VISIBILITY_CSS_VARIABLE_NAME,
'hidden',
);
const currentRecordIds = store.get(recordIndexAllRecordIds);
let records: ObjectRecord[] | null = null;
let totalCount = 0;
if (showAuthModal) {
records = SIGN_IN_BACKGROUND_MOCK_COMPANIES;
totalCount = SIGN_IN_BACKGROUND_MOCK_COMPANIES.length;
} else {
const newRecordIdByRealIndex = new Map(
store.get(recordIdByRealIndexCallbackState),
);
const newDataLoadingStatusByRealIndex = new Map(
store.get(dataLoadingStatusByRealIndexCallbackState),
);
for (const [realIndex] of currentRecordIds.entries()) {
newDataLoadingStatusByRealIndex.set(realIndex, 'not-loaded');
newRecordIdByRealIndex.delete(realIndex);
}
store.set(recordIdByRealIndexCallbackState, newRecordIdByRealIndex);
store.set(
dataLoadingStatusByRealIndexCallbackState,
newDataLoadingStatusByRealIndex,
);
const { records: findManyRecords, totalCount: findManyTotalCount } =
await findManyRecordsLazy();
records = findManyRecords;
totalCount = findManyTotalCount;
}
store.set(recordIdByRealIndexCallbackState, newRecordIdByRealIndex);
store.set(
dataLoadingStatusByRealIndexCallbackState,
newDataLoadingStatusByRealIndex,
);
store.set(totalNumberOfRecordsToVirtualizeCallbackState, totalCount);
const { records: findManyRecords, totalCount: findManyTotalCount } =
await findManyRecordsLazy();
if (isDefined(records)) {
upsertRecordsInStore({ partialRecords: records });
records = findManyRecords;
totalCount = findManyTotalCount;
}
loadRecordsToVirtualRows({
records,
startingRealIndex: 0,
});
store.set(totalNumberOfRecordsToVirtualizeCallbackState, totalCount);
reapplyRowSelection();
}
if (isDefined(records)) {
upsertRecordsInStore({ partialRecords: records });
store.set(dataPagesLoadedCallbackState, []);
loadRecordsToVirtualRows({
records,
startingRealIndex: 0,
});
store.set(lastScrollPositionCallbackState, 0);
store.set(lastRealIndexSetCallbackState, null);
store.set(scrollAtRealIndexCallbackState, 0);
reapplyRowSelection();
}
setIsRecordTableScrolledHorizontally(false);
setIsRecordTableScrolledVertically(false);
resetTableFocuses();
store.set(dataPagesLoadedCallbackState, []);
store.set(isInitializingVirtualTableDataLoadingCallbackState, false);
store.set(isRecordTableInitialLoading, false);
store.set(lastScrollPositionCallbackState, 0);
store.set(lastRealIndexSetCallbackState, null);
store.set(scrollAtRealIndexCallbackState, 0);
setIsRecordTableScrolledHorizontally(false);
setIsRecordTableScrolledVertically(false);
resetTableFocuses();
if (shouldScrollToStart) {
scrollTableToPosition({
horizontalScrollInPx: 0,
verticalScrollInPx: 0,
});
if (shouldScrollToStart) {
scrollTableToPosition({
horizontalScrollInPx: 0,
verticalScrollInPx: 0,
});
}
} finally {
store.set(isInitializingVirtualTableDataLoadingCallbackState, false);
store.set(isRecordTableInitialLoading, false);
}
},
[