From f395e814f291322e041bb93ffb8d645c08d5ca3d Mon Sep 17 00:00:00 2001 From: Zeke Zhang <958414905@qq.com> Date: Mon, 4 Nov 2024 17:06:55 +0800 Subject: [PATCH] perf(react-router-hooks): improve performance --- .../CustomRouterContextProvider.tsx | 111 +++++++++++++++++- .../client/src/application/hooks/index.ts | 1 - .../application/hooks/useRouterBasename.ts | 19 --- .../client/src/hoc/withDynamicSchemaProps.tsx | 4 +- .../core/client/src/pm/PluginManagerLink.tsx | 11 +- .../route-switch/antd/admin-layout/index.tsx | 74 ++++++------ .../schema-component/antd/action/context.tsx | 31 +---- .../src/schema-component/antd/page/Page.tsx | 34 ++++-- .../schema-component/antd/page/PagePopups.tsx | 3 +- .../useGetAriaLabelOfSchemaInitializer.ts | 4 +- .../src/variables/hooks/useContextVariable.ts | 2 +- .../EditCollectionAction.tsx | 2 +- .../src/client/devices/index.tsx | 1 - .../client/demos/pages-page-tabs-false.tsx | 2 - 14 files changed, 186 insertions(+), 113 deletions(-) delete mode 100644 packages/core/client/src/application/hooks/useRouterBasename.ts diff --git a/packages/core/client/src/application/CustomRouterContextProvider.tsx b/packages/core/client/src/application/CustomRouterContextProvider.tsx index 279e3ab412..ace2cdb441 100644 --- a/packages/core/client/src/application/CustomRouterContextProvider.tsx +++ b/packages/core/client/src/application/CustomRouterContextProvider.tsx @@ -7,8 +7,19 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import React, { FC, useEffect } from 'react'; -import { Location, NavigateFunction, NavigateOptions, useLocation, useNavigate, useParams } from 'react-router-dom'; +import { Schema } from '@formily/json-schema'; +import _ from 'lodash'; +import React, { FC, useEffect, useMemo, useRef, useState } from 'react'; +import { + Location, + NavigateFunction, + NavigateOptions, + useHref, + useLocation, + useNavigate, + useParams, + useSearchParams, +} from 'react-router-dom'; const NavigateNoUpdateContext = React.createContext(null); NavigateNoUpdateContext.displayName = 'NavigateNoUpdateContext'; @@ -34,6 +45,61 @@ MatchAdminNameContext.displayName = 'MatchAdminNameContext'; const IsInSettingsPageContext = React.createContext(false); IsInSettingsPageContext.displayName = 'IsInSettingsPageContext'; +const CurrentTabUidContext = React.createContext(''); +CurrentTabUidContext.displayName = 'CurrentTabUidContext'; + +const SearchParamsContext = React.createContext(new URLSearchParams()); +SearchParamsContext.displayName = 'SearchParamsContext'; + +const RouterBasenameContext = React.createContext(''); +RouterBasenameContext.displayName = 'RouterBasenameContext'; + +const IsSubPageClosedByPageMenuContext = React.createContext<{ + isSubPageClosedByPageMenu: boolean; + setFieldSchema: React.Dispatch>; +}>({ + isSubPageClosedByPageMenu: false, + setFieldSchema: () => {}, +}); +IsSubPageClosedByPageMenuContext.displayName = 'IsSubPageClosedByPageMenuContext'; + +export const IsSubPageClosedByPageMenuProvider: FC = ({ children }) => { + const params = useParams(); + const prevParamsRef = useRef({}); + const [fieldSchema, setFieldSchema] = useState(null); + + const isSubPageClosedByPageMenu = useMemo(() => { + const result = + _.isEmpty(params['*']) && + fieldSchema?.['x-component-props']?.openMode === 'page' && + !!prevParamsRef.current['*']?.includes(fieldSchema['x-uid']); + + prevParamsRef.current = params; + + return result; + }, [fieldSchema, params]); + + const value = useMemo(() => ({ isSubPageClosedByPageMenu, setFieldSchema }), [isSubPageClosedByPageMenu]); + + return ( + {children} + ); +}; + +/** + * see: https://stackoverflow.com/questions/50449423/accessing-basename-of-browserouter + * @returns {string} basename + */ +const RouterBasenameProvider: FC = ({ children }) => { + const basenameOfCurrentRouter = useHref('/'); + return {children}; +}; + +const SearchParamsProvider: FC = ({ children }) => { + const [searchParams] = useSearchParams(); + return {children}; +}; + const IsInSettingsPageProvider: FC = ({ children }) => { const isInSettingsPage = useLocation().pathname.includes('/settings'); return {children}; @@ -62,6 +128,11 @@ export const CurrentPageUidProvider: FC = ({ children }) => { return {children}; }; +export const CurrentTabUidProvider: FC = ({ children }) => { + const params = useParams(); + return {children}; +}; + /** * When the URL changes, components that use `useNavigate` will re-render. * This provider provides a `navigateNoUpdate` method that can avoid re-rendering. @@ -107,7 +178,7 @@ const LocationSearchProvider: FC = ({ children }) => { }; /** - * use `useNavigateNoUpdate` to avoid components that use `useNavigateNoUpdate` re-rendering. + * use `useNavigateNoUpdate` to avoid components re-rendering. * @returns */ export const useNavigateNoUpdate = () => { @@ -115,7 +186,7 @@ export const useNavigateNoUpdate = () => { }; /** - * use `useLocationNoUpdate` to avoid components that use `useLocationNoUpdate` re-rendering. + * use `useLocationNoUpdate` to avoid components re-rendering. * @returns */ export const useLocationNoUpdate = () => { @@ -146,6 +217,32 @@ export const useIsInSettingsPage = () => { return React.useContext(IsInSettingsPageContext); }; +export const useCurrentTabUid = () => { + return React.useContext(CurrentTabUidContext); +}; + +export const useCurrentSearchParams = () => { + return React.useContext(SearchParamsContext); +}; + +export const useRouterBasename = () => { + return React.useContext(RouterBasenameContext); +}; + +/** + * Used to determine if the user closed the sub-page by clicking on the page menu + * @returns + */ +export const useIsSubPageClosedByPageMenu = (fieldSchema: Schema) => { + const { isSubPageClosedByPageMenu, setFieldSchema } = React.useContext(IsSubPageClosedByPageMenuContext); + + useEffect(() => { + setFieldSchema(fieldSchema); + }, [fieldSchema, setFieldSchema]); + + return isSubPageClosedByPageMenu; +}; + export const CustomRouterContextProvider: FC = ({ children }) => { return ( @@ -154,7 +251,11 @@ export const CustomRouterContextProvider: FC = ({ children }) => { - {children} + + + {children} + + diff --git a/packages/core/client/src/application/hooks/index.ts b/packages/core/client/src/application/hooks/index.ts index 1a1ce63405..e7d4f06ee0 100644 --- a/packages/core/client/src/application/hooks/index.ts +++ b/packages/core/client/src/application/hooks/index.ts @@ -11,4 +11,3 @@ export * from './useApp'; export * from './useAppSpin'; export * from './usePlugin'; export * from './useRouter'; -export * from './useRouterBasename'; diff --git a/packages/core/client/src/application/hooks/useRouterBasename.ts b/packages/core/client/src/application/hooks/useRouterBasename.ts deleted file mode 100644 index 9460f72b94..0000000000 --- a/packages/core/client/src/application/hooks/useRouterBasename.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * This file is part of the NocoBase (R) project. - * Copyright (c) 2020-2024 NocoBase Co., Ltd. - * Authors: NocoBase Team. - * - * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. - * For more information, please refer to: https://www.nocobase.com/agreement. - */ - -import { useHref } from 'react-router-dom'; - -/** - * see: https://stackoverflow.com/questions/50449423/accessing-basename-of-browserouter - * @returns {string} basename - */ -export const useRouterBasename = () => { - const basenameOfCurrentRouter = useHref('/'); - return basenameOfCurrentRouter; -}; diff --git a/packages/core/client/src/hoc/withDynamicSchemaProps.tsx b/packages/core/client/src/hoc/withDynamicSchemaProps.tsx index 3d7bfdb8dc..adb919012c 100644 --- a/packages/core/client/src/hoc/withDynamicSchemaProps.tsx +++ b/packages/core/client/src/hoc/withDynamicSchemaProps.tsx @@ -62,7 +62,7 @@ export function withDynamicSchemaProps( options: WithSchemaHookOptions = {}, ) { const displayName = options.displayName || Component.displayName || Component.name; - const ComponentWithProps: ComponentType = (props) => { + const ComponentWithProps: ComponentType = React.memo((props) => { const { dn, findComponent } = useDesignable(); const useComponentPropsStr = useMemo(() => { const xComponent = dn.getSchemaAttribute('x-component'); @@ -85,7 +85,7 @@ export function withDynamicSchemaProps( }, [schemaProps, props]); return {props.children}; - }; + }); Component.displayName = displayName; ComponentWithProps.displayName = `withSchemaProps(${displayName})`; diff --git a/packages/core/client/src/pm/PluginManagerLink.tsx b/packages/core/client/src/pm/PluginManagerLink.tsx index 657cad7f78..07867ce568 100644 --- a/packages/core/client/src/pm/PluginManagerLink.tsx +++ b/packages/core/client/src/pm/PluginManagerLink.tsx @@ -11,14 +11,14 @@ import { ApiOutlined, SettingOutlined } from '@ant-design/icons'; import { Button, Dropdown, Tooltip } from 'antd'; import React, { useEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { Link, useNavigate } from 'react-router-dom'; -import { useApp } from '../application'; +import { Link } from 'react-router-dom'; +import { useApp, useNavigateNoUpdate } from '../application'; import { useCompile } from '../schema-component'; import { useToken } from '../style'; export const PluginManagerLink = () => { const { t } = useTranslation(); - const navigate = useNavigate(); + const navigate = useNavigateNoUpdate(); const { token } = useToken(); return ( @@ -47,8 +47,11 @@ export const SettingsCenterDropdown = () => { return { key: setting.name, icon: setting.icon, - label: setting.link ?
window.open(setting.link)}>{compile(setting.title)}
: + label: setting.link ? ( +
window.open(setting.link)}>{compile(setting.title)}
+ ) : ( {compile(setting.title)} + ), }; }); }, [app, t]); diff --git a/packages/core/client/src/route-switch/antd/admin-layout/index.tsx b/packages/core/client/src/route-switch/antd/admin-layout/index.tsx index b1e6821410..42b7c9ac29 100644 --- a/packages/core/client/src/route-switch/antd/admin-layout/index.tsx +++ b/packages/core/client/src/route-switch/antd/admin-layout/index.tsx @@ -35,6 +35,8 @@ import { } from '../../../'; import { CurrentPageUidProvider, + CurrentTabUidProvider, + IsSubPageClosedByPageMenuProvider, useCurrentPageUid, useIsInSettingsPage, useMatchAdmin, @@ -467,41 +469,45 @@ export const InternalAdminLayout = () => { - -
-
-
- {result?.data?.data?.logo?.url ? ( - - ) : ( - - {result?.data?.data?.title} - - )} + + + +
+
+
+ {result?.data?.data?.logo?.url ? ( + + ) : ( + + {result?.data?.data?.title} + + )} +
+
+ + + +
+
+
+ + + + + + +
-
- - - -
-
-
- - - - - - -
-
- - - {/* Use the "nb-subpages-slot-without-header-and-side" class name to locate the position of the subpages */} - -
- - {/* {service.contentLoading ? render() : } */} -
+ + + {/* Use the "nb-subpages-slot-without-header-and-side" class name to locate the position of the subpages */} + +
+ + {/* {service.contentLoading ? render() : } */} +
+ + ); diff --git a/packages/core/client/src/schema-component/antd/action/context.tsx b/packages/core/client/src/schema-component/antd/action/context.tsx index c939511de6..08ba008d33 100644 --- a/packages/core/client/src/schema-component/antd/action/context.tsx +++ b/packages/core/client/src/schema-component/antd/action/context.tsx @@ -8,9 +8,8 @@ */ import { useFieldSchema } from '@formily/react'; -import _ from 'lodash'; -import React, { createContext, useEffect, useMemo, useRef, useState } from 'react'; -import { useParams } from 'react-router-dom'; +import React, { createContext, useEffect, useMemo, useState } from 'react'; +import { useIsSubPageClosedByPageMenu } from '../../../application/CustomRouterContextProvider'; import { useDataBlockRequest } from '../../../data-source'; import { useCurrentPopupContext } from '../page/PagePopups'; import { getBlockService, storeBlockService } from '../page/pagePopupUtils'; @@ -19,37 +18,13 @@ import { ActionContextProps } from './types'; export const ActionContext = createContext({}); ActionContext.displayName = 'ActionContext'; -/** - * Used to determine if the user closed the sub-page by clicking on the page menu - * @returns - */ -const useIsSubPageClosedByPageMenu = () => { - // Used to trigger re-rendering when URL changes - const params = useParams(); - const prevParamsRef = useRef({}); - const fieldSchema = useFieldSchema(); - - const isSubPageClosedByPageMenu = useMemo(() => { - const result = - _.isEmpty(params['*']) && - fieldSchema?.['x-component-props']?.openMode === 'page' && - !!prevParamsRef.current['*']?.includes(fieldSchema['x-uid']); - - prevParamsRef.current = params; - - return result; - }, [fieldSchema, params]); - - return isSubPageClosedByPageMenu; -}; - export const ActionContextProvider: React.FC = React.memo( (props) => { const [submitted, setSubmitted] = useState(false); //是否有提交记录 const { visible } = { ...props, ...props.value } || {}; const { setSubmitted: setParentSubmitted } = { ...props, ...props.value }; const service = useBlockServiceInActionButton(); - const isSubPageClosedByPageMenu = useIsSubPageClosedByPageMenu(); + const isSubPageClosedByPageMenu = useIsSubPageClosedByPageMenu(useFieldSchema()); useEffect(() => { if (visible === false && service && !service.loading && (submitted || isSubPageClosedByPageMenu)) { diff --git a/packages/core/client/src/schema-component/antd/page/Page.tsx b/packages/core/client/src/schema-component/antd/page/Page.tsx index d9c227f03b..fa9d923a86 100644 --- a/packages/core/client/src/schema-component/antd/page/Page.tsx +++ b/packages/core/client/src/schema-component/antd/page/Page.tsx @@ -17,12 +17,16 @@ import classNames from 'classnames'; 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'; +import { NavigateFunction, Outlet, useOutletContext } from 'react-router-dom'; import { FormDialog } from '..'; import { antTableCell } from '../../../acl/style'; import { useRequest } from '../../../api-client'; -import { useNavigateNoUpdate } from '../../../application/CustomRouterContextProvider'; -import { useRouterBasename } from '../../../application/hooks/useRouterBasename'; +import { + useCurrentSearchParams, + useCurrentTabUid, + useNavigateNoUpdate, + useRouterBasename, +} from '../../../application/CustomRouterContextProvider'; import { useDocumentTitle } from '../../../document-title'; import { useGlobalTheme } from '../../../global-theme'; import { Icon } from '../../../icon'; @@ -36,25 +40,24 @@ import { ErrorFallback } from '../error-fallback'; import { useStyles } from './Page.style'; import { PageDesigner, PageTabDesigner } from './PageTabDesigner'; -export const Page = (props) => { +export const Page = React.memo((props: any) => { const { t } = useTranslation(); const fieldSchema = useFieldSchema(); const dn = useDesignable(); const { theme } = useGlobalTheme(); const { getAriaLabel } = useGetAriaLabelOfSchemaInitializer(); - const { tabUid } = useParams(); + const currentTabUid = useCurrentTabUid(); const basenameOfCurrentRouter = useRouterBasename(); - const disablePageHeader = fieldSchema['x-component-props']?.disablePageHeader; const enablePageTabs = fieldSchema['x-component-props']?.enablePageTabs; const options = useContext(SchemaOptionsContext); const navigate = useNavigateNoUpdate(); - const [searchParams] = useSearchParams(); + const searchParams = useCurrentSearchParams(); const loading = false; const activeKey = useMemo( // 处理 searchParams 是为了兼容旧版的 tab 参数 - () => tabUid || searchParams.get('tab') || Object.keys(fieldSchema.properties || {}).shift(), - [fieldSchema.properties, searchParams, tabUid], + () => currentTabUid || searchParams.get('tab') || Object.keys(fieldSchema.properties || {}).shift(), + [fieldSchema.properties, searchParams, currentTabUid], ); const { wrapSSR, hashId, componentCls } = useStyles(); const { token } = useToken(); @@ -168,14 +171,19 @@ export const Page = (props) => { ) : null; }, [activeKey, enablePageTabs, handleTabsChange, items, tabBarExtraContent, tabBarStyle]); + const outletContext = useMemo( + () => ({ loading, disablePageHeader, enablePageTabs, fieldSchema, tabUid: currentTabUid }), + [currentTabUid, disablePageHeader, enablePageTabs, fieldSchema, loading], + ); + return wrapSSR(
- {tabUid ? ( + {currentTabUid ? ( // used to match the rout with name "admin.page.tab" - + ) : ( <> {
, ); -}; +}); + +Page.displayName = 'NocoBasePage'; export const PageTabs = () => { const { loading, disablePageHeader, enablePageTabs, fieldSchema, tabUid } = useOutletContext(); diff --git a/packages/core/client/src/schema-component/antd/page/PagePopups.tsx b/packages/core/client/src/schema-component/antd/page/PagePopups.tsx index 0aab16253c..3a79c92405 100644 --- a/packages/core/client/src/schema-component/antd/page/PagePopups.tsx +++ b/packages/core/client/src/schema-component/antd/page/PagePopups.tsx @@ -109,6 +109,7 @@ const PopupTabsPropsProvider: FC = ({ children }) => { ); }; +const displayNone = { display: 'none' }; const PagePopupsItemProvider: FC<{ params: PopupParams; context: PopupContext; @@ -180,7 +181,7 @@ const PagePopupsItemProvider: FC<{ {/* Pass the service of the block where the button is located down, to refresh the block's data when the popup is closed */} -
{children}
+
{children}
diff --git a/packages/core/client/src/schema-initializer/hooks/useGetAriaLabelOfSchemaInitializer.ts b/packages/core/client/src/schema-initializer/hooks/useGetAriaLabelOfSchemaInitializer.ts index 122c3e743b..b0578ec4c0 100644 --- a/packages/core/client/src/schema-initializer/hooks/useGetAriaLabelOfSchemaInitializer.ts +++ b/packages/core/client/src/schema-initializer/hooks/useGetAriaLabelOfSchemaInitializer.ts @@ -9,7 +9,7 @@ import { useFieldSchema } from '@formily/react'; import { useCallback } from 'react'; -import { useCollection_deprecated } from '../../collection-manager'; +import { useCollection } from '../../data-source/collection/CollectionProvider'; /** * label = 'schema-initializer' + x-component + [x-initializer] + [collectionName] + [postfix] @@ -18,7 +18,7 @@ import { useCollection_deprecated } from '../../collection-manager'; export const useGetAriaLabelOfSchemaInitializer = () => { const fieldSchema = useFieldSchema(); - const { name } = useCollection_deprecated(); + const { name } = useCollection() || {}; const getAriaLabel = useCallback( (postfix?: string) => { if (!fieldSchema) return ''; diff --git a/packages/core/client/src/variables/hooks/useContextVariable.ts b/packages/core/client/src/variables/hooks/useContextVariable.ts index aea4dd176b..d7dfb9261b 100644 --- a/packages/core/client/src/variables/hooks/useContextVariable.ts +++ b/packages/core/client/src/variables/hooks/useContextVariable.ts @@ -34,7 +34,7 @@ const useContextVariable = (): VariableOption => { const { field, blockData, rowKey, collection: collectionName } = tableBlockContext || {}; const contextData = useMemo( - () => blockData?.data?.filter((v) => (field?.data?.selectedRowKeys || [])?.includes(v[rowKey])), + () => blockData?.data?.filter?.((v) => (field?.data?.selectedRowKeys || [])?.includes(v[rowKey])), [field?.data?.selectedRowKeys, rowKey, blockData], ); diff --git a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/EditCollectionAction.tsx b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/EditCollectionAction.tsx index afe3eb3cfc..c49c5f48f5 100644 --- a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/EditCollectionAction.tsx +++ b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/EditCollectionAction.tsx @@ -139,7 +139,7 @@ export const useUpdateCollectionActionAndRefreshCM = (options) => { const ctx = useActionContext(); const { name } = useParams(); const { refresh } = useResourceActionContext(); - const { resource, targetKey } = useResourceContext(); + const { targetKey } = useResourceContext(); const { [targetKey]: filterByTk } = useRecord(); const api = useAPIClient(); const dm = useDataSourceManager(); diff --git a/packages/plugins/@nocobase/plugin-mobile-client/src/client/devices/index.tsx b/packages/plugins/@nocobase/plugin-mobile-client/src/client/devices/index.tsx index b348558951..37a9e3c7bd 100644 --- a/packages/plugins/@nocobase/plugin-mobile-client/src/client/devices/index.tsx +++ b/packages/plugins/@nocobase/plugin-mobile-client/src/client/devices/index.tsx @@ -9,7 +9,6 @@ import { css, cx } from '@nocobase/client'; import React from 'react'; -import { useLocation } from 'react-router-dom'; import Device from './iOS6'; export const MobileDevice: React.FC = (props) => { diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/demos/pages-page-tabs-false.tsx b/packages/plugins/@nocobase/plugin-mobile/src/client/demos/pages-page-tabs-false.tsx index e7f88f2f12..f21fd5f05a 100644 --- a/packages/plugins/@nocobase/plugin-mobile/src/client/demos/pages-page-tabs-false.tsx +++ b/packages/plugins/@nocobase/plugin-mobile/src/client/demos/pages-page-tabs-false.tsx @@ -8,10 +8,8 @@ import { MobileTitleProvider, } from '@nocobase/plugin-mobile/client'; import React from 'react'; -import { useLocation } from 'react-router-dom'; const Demo = () => { - const { pathname } = useLocation(); return (