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 { Button, Tabs } from 'antd';
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 { useTranslation } from 'react-i18next';
import { NavigateFunction, Outlet, useOutletContext, useParams, useSearchParams } from 'react-router-dom';
@ -42,7 +42,7 @@ export const Page = (props) => {
const dn = useDesignable();
const { theme } = useGlobalTheme();
const { getAriaLabel } = useGetAriaLabelOfSchemaInitializer();
const { tabUid, name: pageUid } = useParams();
const { tabUid } = useParams();
const basenameOfCurrentRouter = useRouterBasename();
const disablePageHeader = fieldSchema['x-component-props']?.disablePageHeader;
@ -63,105 +63,114 @@ export const Page = (props) => {
console.error(error);
}, []);
// 这里的样式是为了保证页面 tabs 标签下面的分割线和页面内容对齐(页面内边距可以通过主题编辑器调节)
const tabBarStyle = useMemo(
() => ({
paddingLeft: token.paddingLG - token.paddingPageHorizontal,
paddingRight: token.paddingLG - token.paddingPageHorizontal,
marginLeft: token.paddingPageHorizontal - token.paddingLG,
marginRight: token.paddingPageHorizontal - token.paddingLG,
}),
[token.paddingLG, token.paddingPageHorizontal],
);
const handleTabsChange = useCallback(
(activeKey: string): void => {
navigateToTab({ activeKey, navigate, basename: basenameOfCurrentRouter });
},
[basenameOfCurrentRouter, navigate],
);
const tabBarExtraContent = useMemo(() => {
return (
dn.designable && (
<Button
aria-label={getAriaLabel('tabs')}
icon={<PlusOutlined />}
className={'addTabBtn'}
type={'dashed'}
onClick={async () => {
const values = await FormDialog(
t('Add tab'),
() => {
return (
<SchemaComponentOptions scope={options.scope} components={{ ...options.components }}>
<FormLayout layout={'vertical'}>
<SchemaComponent
schema={{
properties: {
title: {
title: t('Tab name'),
'x-component': 'Input',
'x-decorator': 'FormItem',
required: true,
},
icon: {
title: t('Icon'),
'x-component': 'IconPicker',
'x-decorator': 'FormItem',
},
},
}}
/>
</FormLayout>
</SchemaComponentOptions>
);
},
theme,
).open({
initialValues: {},
});
const { title, icon } = values;
dn.insertBeforeEnd({
type: 'void',
title,
'x-icon': icon,
'x-component': 'Grid',
'x-initializer': 'page:addBlock',
properties: {},
});
}}
>
{t('Add tab')}
</Button>
)
);
}, [dn, getAriaLabel, options?.components, options?.scope, t, theme]);
const items = useMemo(() => {
return fieldSchema.mapProperties((schema) => {
return {
label: (
<SortableItem
id={schema.name as string}
schema={schema}
className={classNames('nb-action-link', 'designerCss', props.className)}
>
{schema['x-icon'] && <Icon style={{ marginRight: 8 }} type={schema['x-icon']} />}
<span>{schema.title || t('Unnamed')}</span>
<PageTabDesigner schema={schema} />
</SortableItem>
),
key: schema.name as string,
};
});
}, [fieldSchema, props.className, t]);
const footer = useMemo(() => {
return enablePageTabs ? (
<DndContext>
<Tabs
size={'small'}
activeKey={activeKey}
// 这里的样式是为了保证页面 tabs 标签下面的分割线和页面内容对齐(页面内边距可以通过主题编辑器调节)
tabBarStyle={{
paddingLeft: token.paddingLG - token.paddingPageHorizontal,
paddingRight: token.paddingLG - token.paddingPageHorizontal,
marginLeft: token.paddingPageHorizontal - token.paddingLG,
marginRight: token.paddingPageHorizontal - token.paddingLG,
}}
onChange={(activeKey) => {
navigateToTab({ activeKey, navigate, basename: basenameOfCurrentRouter });
}}
tabBarExtraContent={
dn.designable && (
<Button
aria-label={getAriaLabel('tabs')}
icon={<PlusOutlined />}
className={'addTabBtn'}
type={'dashed'}
onClick={async () => {
const values = await FormDialog(
t('Add tab'),
() => {
return (
<SchemaComponentOptions scope={options.scope} components={{ ...options.components }}>
<FormLayout layout={'vertical'}>
<SchemaComponent
schema={{
properties: {
title: {
title: t('Tab name'),
'x-component': 'Input',
'x-decorator': 'FormItem',
required: true,
},
icon: {
title: t('Icon'),
'x-component': 'IconPicker',
'x-decorator': 'FormItem',
},
},
}}
/>
</FormLayout>
</SchemaComponentOptions>
);
},
theme,
).open({
initialValues: {},
});
const { title, icon } = values;
dn.insertBeforeEnd({
type: 'void',
title,
'x-icon': icon,
'x-component': 'Grid',
'x-initializer': 'page:addBlock',
properties: {},
});
}}
>
{t('Add tab')}
</Button>
)
}
items={fieldSchema.mapProperties((schema) => {
return {
label: (
<SortableItem
id={schema.name as string}
schema={schema}
className={classNames('nb-action-link', 'designerCss', props.className)}
>
{schema['x-icon'] && <Icon style={{ marginRight: 8 }} type={schema['x-icon']} />}
<span>{schema.title || t('Unnamed')}</span>
<PageTabDesigner schema={schema} />
</SortableItem>
),
key: schema.name as string,
};
})}
tabBarStyle={tabBarStyle}
onChange={handleTabsChange}
tabBarExtraContent={tabBarExtraContent}
items={items}
/>
</DndContext>
) : null;
}, [
activeKey,
fieldSchema,
dn.designable,
options.scope,
options.components,
pageUid,
fieldSchema.mapProperties((schema) => schema.title || t('Unnamed')).join(),
enablePageTabs,
]);
}, [activeKey, enablePageTabs, handleTabsChange, items, tabBarExtraContent, tabBarStyle]);
return wrapSSR(
<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(
({
loading,
@ -231,44 +269,23 @@ const PageContent = memo(
fieldSchema: Schema<any, any, any, any, any, any, any, any, any>;
activeKey: string;
}) => {
// const { render } = useAppSpin();
// if (loading) {
// return render();
// }g
if (!disablePageHeader && enablePageTabs) {
const result = fieldSchema
.mapProperties((schema) => {
if (schema.name !== activeKey) return null;
return (
<SchemaComponent
key={schema.name}
distributed
schema={
new Schema({
properties: {
[schema.name]: schema,
},
})
}
/>
);
})
.filter(Boolean);
return result[0];
} else {
return (
<div className={className1}>
<SchemaComponent schema={fieldSchema} distributed />
</div>
<>
{fieldSchema.mapProperties((schema) => (
<TabPane key={schema.name} schema={schema} active={schema.name === activeKey} />
))}
</>
);
}
return (
<div className={className1}>
<SchemaComponent schema={fieldSchema} distributed />
</div>
);
},
);
PageContent.displayName = 'PageContent';
function NocoBasePageHeader({ footer }: { footer: React.JSX.Element }) {
const fieldSchema = useFieldSchema();