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,18 +1,20 @@
import { useEffect } from 'react';
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useSetRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentFamilyStateV2';
import { useViewFromQueryParams } from '@/views/hooks/internal/useViewFromQueryParams';
import { useApplyViewFiltersToCurrentRecordFilters } from '@/views/hooks/useApplyViewFiltersToCurrentRecordFilters';
import { useResetUnsavedViewStates } from '@/views/hooks/useResetUnsavedViewStates';
import { currentViewIdComponentState } from '@/views/states/currentViewIdComponentState';
import { unsavedToUpsertViewFiltersComponentFamilyState } from '@/views/states/unsavedToUpsertViewFiltersComponentFamilyState';
export const QueryParamsFiltersEffect = () => {
const { hasFiltersQueryParams, getFiltersFromQueryParams, viewIdQueryParam } =
useViewFromQueryParams();
const currentViewId = useRecoilComponentValueV2(currentViewIdComponentState);
const currentViewId = useRecoilComponentValueV2(
contextStoreCurrentViewIdComponentState,
);
const setUnsavedViewFilter = useSetRecoilComponentFamilyStateV2(
unsavedToUpsertViewFiltersComponentFamilyState,

View File

@ -1,35 +0,0 @@
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { mainContextStoreComponentInstanceIdState } from '@/context-store/states/mainContextStoreComponentInstanceId';
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { currentViewIdComponentState } from '@/views/states/currentViewIdComponentState';
import { useEffect } from 'react';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared';
// TODO: This whole code should be removed. currentViewId should be used directly to set the mainContextStore
// and viewbar / view tooling should be updated to use that state contextStore state directly.
export const QueryParamsViewIdEffect = () => {
const [currentViewId, setCurrentViewId] = useRecoilComponentStateV2(
currentViewIdComponentState,
);
const mainContextStoreComponentInstanceId = useRecoilValue(
mainContextStoreComponentInstanceIdState,
);
const contextStoreCurrentViewId = useRecoilComponentValueV2(
contextStoreCurrentViewIdComponentState,
mainContextStoreComponentInstanceId,
);
useEffect(() => {
if (isDefined(contextStoreCurrentViewId)) {
if (currentViewId !== contextStoreCurrentViewId) {
setCurrentViewId(contextStoreCurrentViewId);
}
}
}, [contextStoreCurrentViewId, currentViewId, setCurrentViewId]);
return <></>;
};

View File

@ -7,6 +7,7 @@ import {
MenuItem,
} from 'twenty-ui';
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
@ -19,7 +20,6 @@ import { useAreViewFiltersDifferentFromRecordFilters } from '@/views/hooks/useAr
import { useAreViewSortsDifferentFromRecordSorts } from '@/views/hooks/useAreViewSortsDifferentFromRecordSorts';
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
import { useSaveCurrentViewFiltersAndSorts } from '@/views/hooks/useSaveCurrentViewFiltersAndSorts';
import { currentViewIdComponentState } from '@/views/states/currentViewIdComponentState';
import { VIEW_PICKER_DROPDOWN_ID } from '@/views/view-picker/constants/ViewPickerDropdownId';
import { useViewPickerMode } from '@/views/view-picker/hooks/useViewPickerMode';
import { viewPickerReferenceViewIdComponentState } from '@/views/view-picker/states/viewPickerReferenceViewIdComponentState';
@ -44,7 +44,9 @@ export const UpdateViewButtonGroup = ({
const { setViewPickerMode } = useViewPickerMode();
const currentViewId = useRecoilComponentValueV2(currentViewIdComponentState);
const currentViewId = useRecoilComponentValueV2(
contextStoreCurrentViewIdComponentState,
);
const { closeDropdown: closeUpdateViewButtonDropdown } = useDropdown(
UPDATE_VIEW_BUTTON_DROPDOWN_ID,

View File

@ -7,13 +7,10 @@ import { ObjectSortDropdownButton } from '@/object-record/object-sort-dropdown/c
import { useIsPrefetchLoading } from '@/prefetch/hooks/useIsPrefetchLoading';
import { TopBar } from '@/ui/layout/top-bar/components/TopBar';
import { QueryParamsFiltersEffect } from '@/views/components/QueryParamsFiltersEffect';
import { QueryParamsViewIdEffect } from '@/views/components/QueryParamsViewIdEffect';
import { ViewBarEffect } from '@/views/components/ViewBarEffect';
import { ViewBarFilterEffect } from '@/views/components/ViewBarFilterEffect';
import { ViewBarPageTitle } from '@/views/components/ViewBarPageTitle';
import { ViewBarSkeletonLoader } from '@/views/components/ViewBarSkeletonLoader';
import { ViewBarSortEffect } from '@/views/components/ViewBarSortEffect';
import { GraphQLView } from '@/views/types/GraphQLView';
import { ViewPickerDropdown } from '@/views/view-picker/components/ViewPickerDropdown';
import { ViewsHotkeyScope } from '../types/ViewsHotkeyScope';
@ -22,7 +19,6 @@ import { FiltersHotkeyScope } from '@/object-record/object-filter-dropdown/types
import { VIEW_SORT_DROPDOWN_ID } from '@/object-record/object-sort-dropdown/constants/ViewSortDropdownId';
import { ObjectSortDropdownComponentInstanceContext } from '@/object-record/object-sort-dropdown/states/context/ObjectSortDropdownComponentInstanceContext';
import { ViewBarRecordFilterEffect } from '@/views/components/ViewBarRecordFilterEffect';
import { ViewEventContext } from '@/views/events/contexts/ViewEventContext';
import { UpdateViewButtonGroup } from './UpdateViewButtonGroup';
import { ViewBarDetails } from './ViewBarDetails';
@ -30,14 +26,12 @@ export type ViewBarProps = {
viewBarId: string;
className?: string;
optionsDropdownButton: ReactNode;
onCurrentViewChange: (view: GraphQLView | undefined) => void | Promise<void>;
};
export const ViewBar = ({
viewBarId,
className,
optionsDropdownButton,
onCurrentViewChange,
}: ViewBarProps) => {
const { objectNamePlural } = useParams();
@ -53,53 +47,49 @@ export const ViewBar = ({
<ObjectSortDropdownComponentInstanceContext.Provider
value={{ instanceId: VIEW_SORT_DROPDOWN_ID }}
>
<ViewEventContext.Provider value={{ onCurrentViewChange }}>
<ViewBarRecordFilterEffect />
<ViewBarEffect viewBarId={viewBarId} />
<ViewBarFilterEffect filterDropdownId={filterDropdownId} />
<ViewBarSortEffect />
<QueryParamsFiltersEffect />
<QueryParamsViewIdEffect />
<ViewBarRecordFilterEffect />
<ViewBarFilterEffect filterDropdownId={filterDropdownId} />
<ViewBarSortEffect />
<QueryParamsFiltersEffect />
<ViewBarPageTitle viewBarId={viewBarId} />
<TopBar
className={className}
leftComponent={
loading ? <ViewBarSkeletonLoader /> : <ViewPickerDropdown />
}
rightComponent={
<>
<ObjectFilterDropdownButton
filterDropdownId={filterDropdownId}
hotkeyScope={{
scope: FiltersHotkeyScope.ObjectFilterDropdownButton,
}}
/>
<ObjectSortDropdownButton
hotkeyScope={{
scope: FiltersHotkeyScope.ObjectSortDropdownButton,
}}
/>
{optionsDropdownButton}
</>
}
bottomComponent={
<ViewBarDetails
<ViewBarPageTitle viewBarId={viewBarId} />
<TopBar
className={className}
leftComponent={
loading ? <ViewBarSkeletonLoader /> : <ViewPickerDropdown />
}
rightComponent={
<>
<ObjectFilterDropdownButton
filterDropdownId={filterDropdownId}
hasFilterButton
viewBarId={viewBarId}
objectNamePlural={objectNamePlural}
rightComponent={
<UpdateViewButtonGroup
hotkeyScope={{
scope: ViewsHotkeyScope.UpdateViewButtonDropdown,
}}
/>
}
hotkeyScope={{
scope: FiltersHotkeyScope.ObjectFilterDropdownButton,
}}
/>
}
/>
</ViewEventContext.Provider>
<ObjectSortDropdownButton
hotkeyScope={{
scope: FiltersHotkeyScope.ObjectSortDropdownButton,
}}
/>
{optionsDropdownButton}
</>
}
bottomComponent={
<ViewBarDetails
filterDropdownId={filterDropdownId}
hasFilterButton
viewBarId={viewBarId}
objectNamePlural={objectNamePlural}
rightComponent={
<UpdateViewButtonGroup
hotkeyScope={{
scope: ViewsHotkeyScope.UpdateViewButtonDropdown,
}}
/>
}
/>
}
/>
</ObjectSortDropdownComponentInstanceContext.Provider>
);
};

View File

@ -1,56 +0,0 @@
import { isUndefined } from '@sniptt/guards';
import { useContext, useEffect, useState } from 'react';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { ViewEventContext } from '@/views/events/contexts/ViewEventContext';
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
import { isPersistingViewFieldsComponentState } from '@/views/states/isPersistingViewFieldsComponentState';
import { View } from '@/views/types/View';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
type ViewBarEffectProps = {
viewBarId: string;
};
export const ViewBarEffect = ({ viewBarId }: ViewBarEffectProps) => {
const { currentViewWithCombinedFiltersAndSorts } =
useGetCurrentView(viewBarId);
const { onCurrentViewChange } = useContext(ViewEventContext);
const [currentViewSnapshot, setCurrentViewSnapshot] = useState<
View | undefined
>(undefined);
const isPersistingViewFields = useRecoilComponentValueV2(
isPersistingViewFieldsComponentState,
viewBarId,
);
useEffect(() => {
if (
!isDeeplyEqual(
currentViewWithCombinedFiltersAndSorts,
currentViewSnapshot,
)
) {
if (isUndefined(currentViewWithCombinedFiltersAndSorts)) {
setCurrentViewSnapshot(currentViewWithCombinedFiltersAndSorts);
onCurrentViewChange?.(undefined);
return;
}
if (!isPersistingViewFields) {
setCurrentViewSnapshot(currentViewWithCombinedFiltersAndSorts);
onCurrentViewChange?.(currentViewWithCombinedFiltersAndSorts);
}
}
}, [
currentViewSnapshot,
currentViewWithCombinedFiltersAndSorts,
isPersistingViewFields,
onCurrentViewChange,
]);
return <></>;
};

View File

@ -1,38 +1,24 @@
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
import { contextStoreCurrentObjectMetadataItemComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemComponentState';
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { useFilterableFieldMetadataItems } from '@/object-record/record-filter/hooks/useFilterableFieldMetadataItems';
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData';
import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
import { prefetchViewFromViewIdFamilySelector } from '@/prefetch/states/selector/prefetchViewFromViewIdFamilySelector';
import { useRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyStateV2';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { hasInitializedCurrentRecordFiltersComponentFamilyState } from '@/views/states/hasInitializedCurrentRecordFiltersComponentFamilyState';
import { View } from '@/views/types/View';
import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters';
import { useEffect } from 'react';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared';
export const ViewBarRecordFilterEffect = () => {
const { records: views, isDataPrefetched } = usePrefetchedData<View>(
PrefetchKey.AllViews,
);
const currentViewId = useRecoilComponentValueV2(
contextStoreCurrentViewIdComponentState,
);
const contextStoreCurrentObjectMetadataId = useRecoilComponentValueV2(
contextStoreCurrentObjectMetadataIdComponentState,
);
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const objectMetadataItem = objectMetadataItems.find(
(objectMetadataItem) =>
objectMetadataItem.id === contextStoreCurrentObjectMetadataId,
const contextStoreCurrentObjectMetadataItem = useRecoilComponentValueV2(
contextStoreCurrentObjectMetadataItemComponentState,
);
const [
@ -54,14 +40,21 @@ export const ViewBarRecordFilterEffect = () => {
);
const { filterableFieldMetadataItems } = useFilterableFieldMetadataItems(
objectMetadataItem?.id,
contextStoreCurrentObjectMetadataItem?.id,
);
const currentView = useRecoilValue(
prefetchViewFromViewIdFamilySelector({
viewId: currentViewId ?? '',
}),
);
useEffect(() => {
if (isDataPrefetched && !hasInitializedCurrentRecordFilters) {
const currentView = views.find((view) => view.id === currentViewId);
if (currentView?.objectMetadataId !== objectMetadataItem?.id) {
if (isDefined(currentView) && !hasInitializedCurrentRecordFilters) {
if (
currentView.objectMetadataId !==
contextStoreCurrentObjectMetadataItem?.id
) {
return;
}
@ -76,15 +69,14 @@ export const ViewBarRecordFilterEffect = () => {
}
}
}, [
isDataPrefetched,
views,
currentViewId,
setCurrentRecordFilters,
filterableFieldMetadataItems,
currentRecordFilters,
hasInitializedCurrentRecordFilters,
setHasInitializedCurrentRecordFilters,
objectMetadataItem?.id,
contextStoreCurrentObjectMetadataItem?.id,
currentView,
]);
return null;