perf(PageTabs): cache rendered content to prevent re-rendering

This commit is contained in:
Zeke Zhang 2024-11-01 00:19:22 +08:00
parent 79472624b5
commit f6de620be5

View File

@ -14,7 +14,7 @@ import { FormLayout } from '@formily/antd-v5';
import { Schema, SchemaOptionsContext, useFieldSchema } from '@formily/react'; import { Schema, SchemaOptionsContext, useFieldSchema } from '@formily/react';
import { Button, Tabs } from 'antd'; import { Button, Tabs } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import React, { memo, useCallback, useContext, useEffect, useMemo, useState } from 'react'; import React, { memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
import { ErrorBoundary } from 'react-error-boundary'; import { ErrorBoundary } from 'react-error-boundary';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { NavigateFunction, Outlet, useOutletContext, useParams, useSearchParams } from 'react-router-dom'; import { NavigateFunction, Outlet, useOutletContext, useParams, useSearchParams } from 'react-router-dom';
@ -42,7 +42,7 @@ export const Page = (props) => {
const dn = useDesignable(); const dn = useDesignable();
const { theme } = useGlobalTheme(); const { theme } = useGlobalTheme();
const { getAriaLabel } = useGetAriaLabelOfSchemaInitializer(); const { getAriaLabel } = useGetAriaLabelOfSchemaInitializer();
const { tabUid, name: pageUid } = useParams(); const { tabUid } = useParams();
const basenameOfCurrentRouter = useRouterBasename(); const basenameOfCurrentRouter = useRouterBasename();
const disablePageHeader = fieldSchema['x-component-props']?.disablePageHeader; const disablePageHeader = fieldSchema['x-component-props']?.disablePageHeader;
@ -63,23 +63,26 @@ export const Page = (props) => {
console.error(error); console.error(error);
}, []); }, []);
const footer = useMemo(() => {
return enablePageTabs ? (
<DndContext>
<Tabs
size={'small'}
activeKey={activeKey}
// 这里的样式是为了保证页面 tabs 标签下面的分割线和页面内容对齐(页面内边距可以通过主题编辑器调节) // 这里的样式是为了保证页面 tabs 标签下面的分割线和页面内容对齐(页面内边距可以通过主题编辑器调节)
tabBarStyle={{ const tabBarStyle = useMemo(
() => ({
paddingLeft: token.paddingLG - token.paddingPageHorizontal, paddingLeft: token.paddingLG - token.paddingPageHorizontal,
paddingRight: token.paddingLG - token.paddingPageHorizontal, paddingRight: token.paddingLG - token.paddingPageHorizontal,
marginLeft: token.paddingPageHorizontal - token.paddingLG, marginLeft: token.paddingPageHorizontal - token.paddingLG,
marginRight: token.paddingPageHorizontal - token.paddingLG, marginRight: token.paddingPageHorizontal - token.paddingLG,
}} }),
onChange={(activeKey) => { [token.paddingLG, token.paddingPageHorizontal],
);
const handleTabsChange = useCallback(
(activeKey: string): void => {
navigateToTab({ activeKey, navigate, basename: basenameOfCurrentRouter }); navigateToTab({ activeKey, navigate, basename: basenameOfCurrentRouter });
}} },
tabBarExtraContent={ [basenameOfCurrentRouter, navigate],
);
const tabBarExtraContent = useMemo(() => {
return (
dn.designable && ( dn.designable && (
<Button <Button
aria-label={getAriaLabel('tabs')} aria-label={getAriaLabel('tabs')}
@ -132,8 +135,11 @@ export const Page = (props) => {
{t('Add tab')} {t('Add tab')}
</Button> </Button>
) )
} );
items={fieldSchema.mapProperties((schema) => { }, [dn, getAriaLabel, options?.components, options?.scope, t, theme]);
const items = useMemo(() => {
return fieldSchema.mapProperties((schema) => {
return { return {
label: ( label: (
<SortableItem <SortableItem
@ -148,20 +154,23 @@ export const Page = (props) => {
), ),
key: schema.name as string, key: schema.name as string,
}; };
})} });
}, [fieldSchema, props.className, t]);
const footer = useMemo(() => {
return enablePageTabs ? (
<DndContext>
<Tabs
size={'small'}
activeKey={activeKey}
tabBarStyle={tabBarStyle}
onChange={handleTabsChange}
tabBarExtraContent={tabBarExtraContent}
items={items}
/> />
</DndContext> </DndContext>
) : null; ) : null;
}, [ }, [activeKey, enablePageTabs, handleTabsChange, items, tabBarExtraContent, tabBarStyle]);
activeKey,
fieldSchema,
dn.designable,
options.scope,
options.components,
pageUid,
fieldSchema.mapProperties((schema) => schema.title || t('Unnamed')).join(),
enablePageTabs,
]);
return wrapSSR( return wrapSSR(
<div className={`${componentCls} ${hashId} ${antTableCell}`}> <div className={`${componentCls} ${hashId} ${antTableCell}`}>
@ -217,6 +226,35 @@ const className1 = css`
} }
`; `;
// Add a TabPane component to manage caching, implementing an effect similar to Vue's keep-alive
const TabPane = React.memo(({ schema, active }: { schema: Schema; active: boolean }) => {
const mountedRef = useRef(false);
if (active && !mountedRef.current) {
mountedRef.current = true;
}
const newSchema = useMemo(
() =>
new Schema({
properties: {
[schema.name]: schema,
},
}),
[schema],
);
if (!mountedRef.current) {
return null;
}
return (
<div style={{ display: active ? 'block' : 'none' }}>
<SchemaComponent distributed schema={newSchema} />
</div>
);
});
const PageContent = memo( const PageContent = memo(
({ ({
loading, loading,
@ -231,44 +269,23 @@ const PageContent = memo(
fieldSchema: Schema<any, any, any, any, any, any, any, any, any>; fieldSchema: Schema<any, any, any, any, any, any, any, any, any>;
activeKey: string; activeKey: string;
}) => { }) => {
// const { render } = useAppSpin();
// if (loading) {
// return render();
// }g
if (!disablePageHeader && enablePageTabs) { if (!disablePageHeader && enablePageTabs) {
const result = fieldSchema
.mapProperties((schema) => {
if (schema.name !== activeKey) return null;
return ( return (
<SchemaComponent <>
key={schema.name} {fieldSchema.mapProperties((schema) => (
distributed <TabPane key={schema.name} schema={schema} active={schema.name === activeKey} />
schema={ ))}
new Schema({ </>
properties: {
[schema.name]: schema,
},
})
}
/>
); );
}) }
.filter(Boolean);
return result[0];
} else {
return ( return (
<div className={className1}> <div className={className1}>
<SchemaComponent schema={fieldSchema} distributed /> <SchemaComponent schema={fieldSchema} distributed />
</div> </div>
); );
}
}, },
); );
PageContent.displayName = 'PageContent';
function NocoBasePageHeader({ footer }: { footer: React.JSX.Element }) { function NocoBasePageHeader({ footer }: { footer: React.JSX.Element }) {
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();