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,24 +0,0 @@
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { useEffect } from 'react';
export const ContextStoreCurrentViewTypeEffect = ({
viewType,
}: {
viewType: ContextStoreViewType | null;
}) => {
const setContextStoreCurrentViewType = useSetRecoilComponentStateV2(
contextStoreCurrentViewTypeComponentState,
);
useEffect(() => {
setContextStoreCurrentViewType(viewType);
return () => {
setContextStoreCurrentViewType(null);
};
}, [setContextStoreCurrentViewType, viewType]);
return null;
};

View File

@ -1,20 +1,16 @@
import { MainContextStoreProviderEffect } from '@/context-store/components/MainContextStoreProviderEffect';
import { useLastVisitedView } from '@/navigation/hooks/useLastVisitedView';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData';
import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
import { prefetchIndexViewIdFromObjectMetadataItemFamilySelector } from '@/prefetch/states/selector/prefetchIndexViewIdFromObjectMetadataItemFamilySelector';
import { AppPath } from '@/types/AppPath';
import { View } from '@/views/types/View';
import { isNonEmptyString } from '@sniptt/guards';
import { useParams, useSearchParams } from 'react-router-dom';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared';
import { undefined } from 'zod';
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
const getViewId = (
viewIdFromQueryParams: string | null,
indexView?: View,
indexViewId?: string,
lastVisitedViewId?: string,
) => {
if (isDefined(viewIdFromQueryParams)) {
@ -25,8 +21,8 @@ const getViewId = (
return lastVisitedViewId;
}
if (isDefined(indexView)) {
return indexView.id;
if (isDefined(indexViewId)) {
return indexViewId;
}
return undefined;
@ -44,17 +40,17 @@ export const MainContextStoreProvider = () => {
: undefined;
const objectNamePlural = useParams().objectNamePlural ?? '';
const objectNameSingular = useParams().objectNameSingular ?? '';
const [searchParams] = useSearchParams();
const viewIdQueryParam = searchParams.get('viewId');
// Todo: this is triggering a lot of re-renders as we update the viewFields, we should introduce a state here
const { records: views } = usePrefetchedData<View>(PrefetchKey.AllViews);
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const objectMetadataItem = objectMetadataItems.find(
(objectMetadataItem) => objectMetadataItem.namePlural === objectNamePlural,
(objectMetadataItem) =>
objectMetadataItem.namePlural === objectNamePlural ||
objectMetadataItem.nameSingular === objectNameSingular,
);
const { getLastVisitedViewIdFromObjectNamePlural } = useLastVisitedView();
@ -63,30 +59,24 @@ export const MainContextStoreProvider = () => {
objectMetadataItem?.namePlural ?? '',
);
const viewsOnCurrentObject = views.filter(
(view) => view.objectMetadataId === objectMetadataItem?.id,
const indexViewId = useRecoilValue(
prefetchIndexViewIdFromObjectMetadataItemFamilySelector({
objectMetadataItemId: objectMetadataItem?.id,
}),
);
const indexView = viewsOnCurrentObject.find((view) => view.key === 'INDEX');
const viewId = getViewId(viewIdQueryParam, indexView, lastVisitedViewId);
const viewId = getViewId(viewIdQueryParam, indexViewId, lastVisitedViewId);
const mainContextStoreComponentInstanceId = `${pageName}-${objectMetadataItem?.namePlural}-${viewId}`;
if (
!isDefined(pageName) ||
!isDefined(objectMetadataItem) ||
!isNonEmptyString(viewId)
) {
if (!isDefined(pageName) || !isDefined(objectMetadataItem)) {
return null;
}
return (
<MainContextStoreProviderEffect
mainContextStoreComponentInstanceIdToSet={
mainContextStoreComponentInstanceId
}
mainContextStoreComponentInstanceIdToSet={'main-context-store'}
viewId={viewId}
objectMetadataItem={objectMetadataItem}
pageName={pageName}
/>
);
};

View File

@ -1,21 +1,27 @@
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
import { contextStoreCurrentObjectMetadataItemComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemComponentState';
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
import { mainContextStoreComponentInstanceIdState } from '@/context-store/states/mainContextStoreComponentInstanceId';
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
import { useSetLastVisitedObjectMetadataId } from '@/navigation/hooks/useSetLastVisitedObjectMetadataId';
import { useSetLastVisitedViewForObjectMetadataNamePlural } from '@/navigation/hooks/useSetLastVisitedViewForObjectMetadataNamePlural';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { prefetchViewFromViewIdFamilySelector } from '@/prefetch/states/selector/prefetchViewFromViewIdFamilySelector';
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
import { ViewType } from '@/views/types/ViewType';
import { useEffect } from 'react';
import { useRecoilState } from 'recoil';
import { useRecoilState, useRecoilValue } from 'recoil';
export const MainContextStoreProviderEffect = ({
mainContextStoreComponentInstanceIdToSet,
viewId,
objectMetadataItem,
pageName,
}: {
mainContextStoreComponentInstanceIdToSet: string;
viewId: string;
viewId?: string;
objectMetadataItem: ObjectMetadataItem;
pageName: string;
}) => {
const [
mainContextStoreComponentInstanceId,
@ -34,17 +40,29 @@ export const MainContextStoreProviderEffect = ({
mainContextStoreComponentInstanceId,
);
const [contextStoreCurrentViewType, setContextStoreCurrentViewType] =
useRecoilComponentStateV2(
contextStoreCurrentViewTypeComponentState,
mainContextStoreComponentInstanceId,
);
const [
contextStoreCurrentObjectMetadataId,
setContextStoreCurrentObjectMetadataId,
contextStoreCurrentObjectMetadataItem,
setContextStoreCurrentObjectMetadataItem,
] = useRecoilComponentStateV2(
contextStoreCurrentObjectMetadataIdComponentState,
contextStoreCurrentObjectMetadataItemComponentState,
mainContextStoreComponentInstanceId,
);
const view = useRecoilValue(
prefetchViewFromViewIdFamilySelector({
viewId: viewId ?? '',
}),
);
useEffect(() => {
if (contextStoreCurrentObjectMetadataId !== objectMetadataItem.id) {
setContextStoreCurrentObjectMetadataId(objectMetadataItem.id);
if (contextStoreCurrentObjectMetadataItem?.id !== objectMetadataItem.id) {
setContextStoreCurrentObjectMetadataItem(objectMetadataItem);
}
if (
@ -58,7 +76,7 @@ export const MainContextStoreProviderEffect = ({
setLastVisitedViewForObjectMetadataNamePlural({
objectNamePlural: objectMetadataItem.namePlural,
viewId: viewId,
viewId: viewId ?? '',
});
setLastVisitedObjectMetadataId({
@ -69,13 +87,13 @@ export const MainContextStoreProviderEffect = ({
setContextStoreCurrentViewId(viewId);
}
}, [
contextStoreCurrentObjectMetadataId,
contextStoreCurrentObjectMetadataItem,
contextStoreCurrentViewId,
mainContextStoreComponentInstanceId,
mainContextStoreComponentInstanceIdToSet,
objectMetadataItem,
objectMetadataItem.namePlural,
setContextStoreCurrentObjectMetadataId,
setContextStoreCurrentObjectMetadataItem,
setContextStoreCurrentViewId,
setLastVisitedObjectMetadataId,
setLastVisitedViewForObjectMetadataNamePlural,
@ -83,5 +101,23 @@ export const MainContextStoreProviderEffect = ({
viewId,
]);
useEffect(() => {
const viewType =
pageName === 'record-show'
? ContextStoreViewType.ShowPage
: view && view.type === ViewType.Kanban
? ContextStoreViewType.Kanban
: ContextStoreViewType.Table;
if (contextStoreCurrentViewType !== viewType) {
setContextStoreCurrentViewType(viewType);
}
}, [
contextStoreCurrentViewType,
pageName,
setContextStoreCurrentViewType,
view,
]);
return null;
};

View File

@ -1,17 +0,0 @@
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
export const useContextStoreCurrentObjectMetadataIdOrThrow = (
instanceId?: string,
) => {
const contextStoreCurrentObjectMetadataId = useRecoilComponentValueV2(
contextStoreCurrentObjectMetadataIdComponentState,
instanceId,
);
if (!contextStoreCurrentObjectMetadataId) {
throw new Error('contextStoreCurrentObjectMetadataIdComponent is not set');
}
return contextStoreCurrentObjectMetadataId;
};

View File

@ -0,0 +1,17 @@
import { contextStoreCurrentObjectMetadataItemComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
export const useContextStoreObjectMetadataItemOrThrow = (
contextStoreInstanceId?: string,
) => {
const objectMetadataItem = useRecoilComponentValueV2(
contextStoreCurrentObjectMetadataItemComponentState,
contextStoreInstanceId,
);
if (!objectMetadataItem) {
throw new Error('Object metadata item is not set in context store');
}
return { objectMetadataItem };
};

View File

@ -1,8 +1,7 @@
import { useContextStoreCurrentObjectMetadataIdOrThrow } from '@/context-store/hooks/useContextStoreCurrentObjectMetadataIdOrThrow';
import { contextStoreCurrentObjectMetadataItemComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemComponentState';
import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters';
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
@ -14,12 +13,10 @@ export const useFindManyRecordsSelectedInContextStore = ({
instanceId?: string;
limit?: number;
}) => {
const objectMetadataId =
useContextStoreCurrentObjectMetadataIdOrThrow(instanceId);
const { objectMetadataItem } = useObjectMetadataItemById({
objectId: objectMetadataId,
});
const objectMetadataItem = useRecoilComponentValueV2(
contextStoreCurrentObjectMetadataItemComponentState,
instanceId,
);
const contextStoreTargetedRecordsRule = useRecoilComponentValueV2(
contextStoreTargetedRecordsRuleComponentState,
@ -36,12 +33,13 @@ export const useFindManyRecordsSelectedInContextStore = ({
const queryFilter = computeContextStoreFilters(
contextStoreTargetedRecordsRule,
contextStoreFilters,
objectMetadataItem,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
objectMetadataItem!,
filterValueDependencies,
);
const { records, loading, totalCount } = useFindManyRecords({
objectNameSingular: objectMetadataItem.nameSingular,
objectNameSingular: objectMetadataItem?.nameSingular ?? '',
filter: queryFilter,
withSoftDeleted: true,
orderBy: [

View File

@ -1,9 +1,10 @@
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
export const contextStoreCurrentObjectMetadataIdComponentState =
createComponentStateV2<string | null>({
key: 'contextStoreCurrentObjectMetadataIdComponentState',
defaultValue: null,
export const contextStoreCurrentObjectMetadataItemComponentState =
createComponentStateV2<ObjectMetadataItem | undefined>({
key: 'contextStoreCurrentObjectMetadataItemComponentState',
defaultValue: undefined,
componentInstanceContext: ContextStoreComponentInstanceContext,
});

View File

@ -2,9 +2,9 @@ import { ContextStoreComponentInstanceContext } from '@/context-store/states/con
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
export const contextStoreCurrentViewIdComponentState = createComponentStateV2<
string | null
string | undefined
>({
key: 'contextStoreCurrentViewIdComponentState',
defaultValue: null,
defaultValue: undefined,
componentInstanceContext: ContextStoreComponentInstanceContext,
});