From d73fc5c08f06c84cb46c74520e6aed68d8ad8056 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 13 Mar 2026 09:25:04 +0000 Subject: [PATCH] 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 --- .../hooks/useFindDuplicatesRecordsQuery.ts | 6 +- .../hooks/useLazyFindManyRecords.ts | 24 +++- .../useTriggerInitialRecordTableDataLoad.ts | 134 +++++++++--------- 3 files changed, 96 insertions(+), 68 deletions(-) diff --git a/packages/twenty-front/src/modules/object-record/hooks/useFindDuplicatesRecordsQuery.ts b/packages/twenty-front/src/modules/object-record/hooks/useFindDuplicatesRecordsQuery.ts index 4220fb9ce96..fd3697718b8 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useFindDuplicatesRecordsQuery.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useFindDuplicatesRecordsQuery.ts @@ -47,7 +47,11 @@ export const useFindDuplicateRecordsQuery = ({ } } `, - [objectMetadataItem, objectMetadataItems, objectPermissionsByObjectMetadataId], + [ + objectMetadataItem, + objectMetadataItems, + objectPermissionsByObjectMetadataId, + ], ); return { diff --git a/packages/twenty-front/src/modules/object-record/hooks/useLazyFindManyRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useLazyFindManyRecords.ts index f8f48d80dcd..5f82dfa8ff7 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useLazyFindManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useLazyFindManyRecords.ts @@ -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 = ({ }; } - 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); } diff --git a/packages/twenty-front/src/modules/object-record/record-table/virtualization/hooks/useTriggerInitialRecordTableDataLoad.ts b/packages/twenty-front/src/modules/object-record/record-table/virtualization/hooks/useTriggerInitialRecordTableDataLoad.ts index 655fb4e7148..85620863743 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/virtualization/hooks/useTriggerInitialRecordTableDataLoad.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/virtualization/hooks/useTriggerInitialRecordTableDataLoad.ts @@ -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); } }, [