Refacto views (#10272)

In this huge (sorry!) PR:
- introducing objectMetadataItem in contextStore instead of
objectMetadataId which is more convenient
- splitting some big hooks into smaller parts to avoid re-renders
- removing Effects to avoid re-renders (especially onViewChange)
- making the view prefetch separate from favorites to avoid re-renders
- making the view prefetch load a state and add selectors on top of it
to avoir re-renders

As a result, the performance is WAY better (I suspect the favorite
implementation to trigger a lot of re-renders unfortunately).
However, we are still facing a random app freeze on view creation. I
could not investigate the root cause. As this seems to be already there
in the precedent release, we can move forward but this seems a urgent
follow up to me ==> EDIT: I've found the root cause after a few ours of
deep dive... an infinite loop in RecordTableNoRecordGroupBodyEffect...

prastoin edit: close https://github.com/twentyhq/twenty/issues/10253

---------

Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
Co-authored-by: prastoin <paul@twenty.com>
This commit is contained in:
Charles Bochet
2025-02-18 13:51:07 +01:00
committed by GitHub
parent 103dff4bd0
commit fb42046033
125 changed files with 1607 additions and 1582 deletions

View File

@ -1,47 +1,13 @@
import { Favorite } from '@/favorites/types/Favorite';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useReadFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useReadFindManyRecordsQueryInCache';
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
import { PREFETCH_CONFIG } from '@/prefetch/constants/PrefetchConfig';
import { useUpsertRecordsInCacheForPrefetchKey } from '@/prefetch/hooks/internal/useUpsertRecordsInCacheForPrefetchKey';
import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
export const useDeleteFavoriteFolder = () => {
const { deleteOneRecord } = useDeleteOneRecord({
objectNameSingular: CoreObjectNameSingular.FavoriteFolder,
});
const { upsertRecordsInCache } =
useUpsertRecordsInCacheForPrefetchKey<Favorite>({
prefetchKey: PrefetchKey.AllFavorites,
});
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular:
PREFETCH_CONFIG[PrefetchKey.AllFavorites].objectNameSingular,
});
const { readFindManyRecordsQueryInCache } =
useReadFindManyRecordsQueryInCache({
objectMetadataItem,
});
const deleteFavoriteFolder = async (folderId: string): Promise<void> => {
await deleteOneRecord(folderId);
const allFavorites = readFindManyRecordsQueryInCache<Favorite>({
queryVariables: {},
recordGqlFields: PREFETCH_CONFIG[
PrefetchKey.AllFavorites
].operationSignatureFactory({ objectMetadataItem }).fields,
});
const updatedFavorites = allFavorites.filter(
(favorite) => favorite.favoriteFolderId !== folderId,
);
upsertRecordsInCache(updatedFavorites);
};
return {

View File

@ -1,11 +1,9 @@
import { favoriteViewsWithMinimalDataSelector } from '@/favorites/states/selectors/favoriteViewsWithMinimalDataSelector';
import { sortFavorites } from '@/favorites/utils/sortFavorites';
import { useGetObjectRecordIdentifierByNameSingular } from '@/object-metadata/hooks/useGetObjectRecordIdentifierByNameSingular';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData';
import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
import { View } from '@/views/types/View';
import { useMemo } from 'react';
import { useRecoilValue } from 'recoil';
import { FieldMetadataType } from '~/generated-metadata/graphql';
@ -13,7 +11,9 @@ import { usePrefetchedFavoritesData } from './usePrefetchedFavoritesData';
export const useFavorites = () => {
const { favorites } = usePrefetchedFavoritesData();
const { records: views } = usePrefetchedData<View>(PrefetchKey.AllViews);
const favoriteViewsWithMinimalData = useRecoilValue(
favoriteViewsWithMinimalDataSelector,
);
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const { objectMetadataItem: favoriteObjectMetadataItem } =
useObjectMetadataItem({
@ -40,14 +40,14 @@ export const useFavorites = () => {
favoriteRelationFieldMetadataItems,
getObjectRecordIdentifierByNameSingular,
true,
views,
favoriteViewsWithMinimalData,
objectMetadataItems,
),
[
favorites,
favoriteRelationFieldMetadataItems,
getObjectRecordIdentifierByNameSingular,
views,
favoriteViewsWithMinimalData,
objectMetadataItems,
],
);

View File

@ -1,4 +1,6 @@
import { sortFavorites } from '@/favorites/utils/sortFavorites';
import { prefetchViewsState } from '@/prefetch/states/prefetchViewsState';
import { useRecoilValue } from 'recoil';
import { useFavoritesMetadata } from './useFavoritesMetadata';
import { usePrefetchedFavoritesData } from './usePrefetchedFavoritesData';
import { usePrefetchedFavoritesFoldersData } from './usePrefetchedFavoritesFoldersData';
@ -7,12 +9,13 @@ export const useFavoritesByFolder = () => {
const { favorites } = usePrefetchedFavoritesData();
const { favoriteFolders } = usePrefetchedFavoritesFoldersData();
const {
views,
objectMetadataItems,
getObjectRecordIdentifierByNameSingular,
favoriteRelationFields,
} = useFavoritesMetadata();
const prefetchViews = useRecoilValue(prefetchViewsState);
const favoritesByFolder = favoriteFolders.map((folder) => ({
folderId: folder.id,
folderName: folder.name,
@ -21,7 +24,7 @@ export const useFavoritesByFolder = () => {
favoriteRelationFields,
getObjectRecordIdentifierByNameSingular,
true,
views,
prefetchViews,
objectMetadataItems,
),
}));

View File

@ -2,14 +2,10 @@ import { useGetObjectRecordIdentifierByNameSingular } from '@/object-metadata/ho
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData';
import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
import { View } from '@/views/types/View';
import { useRecoilValue } from 'recoil';
import { FieldMetadataType } from '~/generated-metadata/graphql';
export const useFavoritesMetadata = () => {
const { records: views } = usePrefetchedData<View>(PrefetchKey.AllViews);
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const getObjectRecordIdentifierByNameSingular =
useGetObjectRecordIdentifierByNameSingular();
@ -27,7 +23,6 @@ export const useFavoritesMetadata = () => {
);
return {
views,
objectMetadataItems,
getObjectRecordIdentifierByNameSingular,
favoriteRelationFields,

View File

@ -1,46 +1,30 @@
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { Favorite } from '@/favorites/types/Favorite';
import { useUpsertRecordsInCacheForPrefetchKey } from '@/prefetch/hooks/internal/useUpsertRecordsInCacheForPrefetchKey';
import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData';
import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
import { prefetchFavoritesState } from '@/prefetch/states/prefetchFavoritesState';
import { useRecoilValue } from 'recoil';
type PrefetchedFavoritesData = {
favorites: Favorite[];
workspaceFavorites: Favorite[];
upsertFavorites: (records: Favorite[]) => void;
currentWorkspaceMemberId: string | undefined;
};
export const usePrefetchedFavoritesData = (): PrefetchedFavoritesData => {
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const currentWorkspaceMemberId = currentWorkspaceMember?.id;
const { records: _favorites } = usePrefetchedData<Favorite>(
PrefetchKey.AllFavorites,
{
workspaceMemberId: {
eq: currentWorkspaceMemberId,
},
},
);
const prefetchFavorites = useRecoilValue(prefetchFavoritesState);
const favorites = _favorites.filter(
const favorites = prefetchFavorites.filter(
(favorite) => favorite.workspaceMemberId === currentWorkspaceMemberId,
);
const workspaceFavorites = _favorites.filter(
const workspaceFavorites = prefetchFavorites.filter(
(favorite) => favorite.workspaceMemberId === null,
);
const { upsertRecordsInCache: upsertFavorites } =
useUpsertRecordsInCacheForPrefetchKey<Favorite>({
prefetchKey: PrefetchKey.AllFavorites,
});
return {
favorites,
workspaceFavorites,
upsertFavorites,
currentWorkspaceMemberId,
};
};

View File

@ -1,13 +1,10 @@
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { FavoriteFolder } from '@/favorites/types/FavoriteFolder';
import { useUpsertRecordsInCacheForPrefetchKey } from '@/prefetch/hooks/internal/useUpsertRecordsInCacheForPrefetchKey';
import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData';
import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
import { prefetchFavoriteFoldersState } from '@/prefetch/states/prefetchFavoriteFoldersState';
import { useRecoilValue } from 'recoil';
type PrefetchedFavoritesFoldersData = {
favoriteFolders: FavoriteFolder[];
upsertFavoriteFolders: (records: FavoriteFolder[]) => void;
currentWorkspaceMemberId: string | undefined;
};
@ -16,23 +13,12 @@ export const usePrefetchedFavoritesFoldersData =
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const currentWorkspaceMemberId = currentWorkspaceMember?.id;
const { records: favoriteFolders } = usePrefetchedData<FavoriteFolder>(
PrefetchKey.AllFavoritesFolders,
{
workspaceMemberId: {
eq: currentWorkspaceMemberId,
},
},
const prefetchFavoriteFolders = useRecoilValue(
prefetchFavoriteFoldersState,
);
const { upsertRecordsInCache: upsertFavoriteFolders } =
useUpsertRecordsInCacheForPrefetchKey<FavoriteFolder>({
prefetchKey: PrefetchKey.AllFavoritesFolders,
});
return {
favoriteFolders,
upsertFavoriteFolders,
favoriteFolders: prefetchFavoriteFolders,
currentWorkspaceMemberId,
};
};

View File

@ -1,32 +1,35 @@
import { sortFavorites } from '@/favorites/utils/sortFavorites';
import { prefetchViewsState } from '@/prefetch/states/prefetchViewsState';
import { useMemo } from 'react';
import { useRecoilValue } from 'recoil';
import { useFavoritesMetadata } from './useFavoritesMetadata';
import { usePrefetchedFavoritesData } from './usePrefetchedFavoritesData';
export const useSortedFavorites = () => {
const { favorites, workspaceFavorites } = usePrefetchedFavoritesData();
const {
views,
objectMetadataItems,
getObjectRecordIdentifierByNameSingular,
favoriteRelationFields,
} = useFavoritesMetadata();
const prefetchViews = useRecoilValue(prefetchViewsState);
const favoritesSorted = useMemo(() => {
return sortFavorites(
favorites,
favoriteRelationFields,
getObjectRecordIdentifierByNameSingular,
true,
views,
prefetchViews,
objectMetadataItems,
);
}, [
favoriteRelationFields,
favorites,
getObjectRecordIdentifierByNameSingular,
views,
objectMetadataItems,
prefetchViews,
]);
const workspaceFavoritesSorted = useMemo(() => {
@ -35,14 +38,14 @@ export const useSortedFavorites = () => {
favoriteRelationFields,
getObjectRecordIdentifierByNameSingular,
false,
views,
prefetchViews,
objectMetadataItems,
);
}, [
workspaceFavorites,
favoriteRelationFields,
getObjectRecordIdentifierByNameSingular,
workspaceFavorites,
views,
prefetchViews,
objectMetadataItems,
]);

View File

@ -4,9 +4,7 @@ import { useGetObjectRecordIdentifierByNameSingular } from '@/object-metadata/ho
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData';
import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
import { View } from '@/views/types/View';
import { prefetchViewsState } from '@/prefetch/states/prefetchViewsState';
import { useMemo } from 'react';
import { useRecoilValue } from 'recoil';
import { FieldMetadataType } from '~/generated-metadata/graphql';
@ -14,8 +12,7 @@ import { usePrefetchedFavoritesData } from './usePrefetchedFavoritesData';
export const useWorkspaceFavorites = () => {
const { workspaceFavorites } = usePrefetchedFavoritesData();
const { records: views } = usePrefetchedData<View>(PrefetchKey.AllViews);
const prefetchViews = useRecoilValue(prefetchViewsState);
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const { objectMetadataItem: favoriteObjectMetadataItem } =
useObjectMetadataItem({
@ -42,14 +39,14 @@ export const useWorkspaceFavorites = () => {
favoriteRelationFieldMetadataItems,
getObjectRecordIdentifierByNameSingular,
false,
views,
prefetchViews,
objectMetadataItems,
),
[
workspaceFavorites,
favoriteRelationFieldMetadataItems,
getObjectRecordIdentifierByNameSingular,
views,
prefetchViews,
objectMetadataItems,
],
);
@ -59,7 +56,7 @@ export const useWorkspaceFavorites = () => {
);
const favoriteViewObjectMetadataIds = new Set(
views.reduce<string[]>((acc, view) => {
prefetchViews.reduce<string[]>((acc, view) => {
if (workspaceFavoriteIds.has(view.id)) {
acc.push(view.objectMetadataId);
}

View File

@ -0,0 +1,18 @@
import { prefetchViewsState } from '@/prefetch/states/prefetchViewsState';
import { View } from '@/views/types/View';
import { selector } from 'recoil';
export const favoriteViewsWithMinimalDataSelector = selector<
Pick<View, 'id' | 'name' | 'objectMetadataId' | 'icon'>[]
>({
key: 'favoriteViewsWithMinimalDataSelector',
get: ({ get }) => {
const views = get(prefetchViewsState);
return views.map((view) => ({
id: view.id,
name: view.name,
objectMetadataId: view.objectMetadataId,
icon: view.icon,
}));
},
});

View File

@ -4,23 +4,13 @@ import { isDefined } from 'twenty-shared';
type ReturnType = {
labelPlural: string;
view: View | null;
view: Pick<View, 'id' | 'name' | 'objectMetadataId'>;
};
export const getObjectMetadataLabelPluralFromViewId = (
views: View[],
view: Pick<View, 'id' | 'name' | 'objectMetadataId'>,
objectMetadataItems: ObjectMetadataItem[],
viewId: string,
): ReturnType => {
const view = views.find((view) => view.id === viewId);
if (!view) {
return {
labelPlural: '',
view: null,
};
}
const objectMetadataItem = objectMetadataItems.find(
(objectMetadataItem) => objectMetadataItem.id === view.objectMetadataId,
);

View File

@ -22,16 +22,23 @@ export const sortFavorites = (
objectNameSingular: string,
) => ObjectRecordIdentifier,
hasLinkToShowPage: boolean,
views: View[],
views: Pick<View, 'id' | 'name' | 'objectMetadataId' | 'icon'>[],
objectMetadataItems: ObjectMetadataItem[],
) => {
return favorites
.map((favorite) => {
if (isDefined(favorite.viewId) && isDefined(favorite.workspaceMemberId)) {
const { labelPlural, view } = getObjectMetadataLabelPluralFromViewId(
views,
const view = views.find((view) => view.id === favorite.viewId);
if (!isDefined(view)) {
return {
...favorite,
} as ProcessedFavorite;
}
const { labelPlural } = getObjectMetadataLabelPluralFromViewId(
view,
objectMetadataItems,
favorite.viewId,
);
return {