From 28e98b23340162cbcf22e61259208efd0f6d3334 Mon Sep 17 00:00:00 2001 From: afc163 Date: Sat, 7 Mar 2026 10:25:54 +0800 Subject: [PATCH] perf: optimize React hooks and fix event listener issues - Account settings: fix event listener closure capturing stale state - ActiveChart: remove unnecessary requestAnimationFrame wrapper, memoize sorted values - TagSelect: memoize getAllTags() result, use Set for O(1) lookups Co-Authored-By: Claude Opus 4.6 --- src/pages/account/settings/index.tsx | 22 +++++------ .../monitor/components/ActiveChart/index.tsx | 39 ++++++++++--------- .../components/TagSelect/index.tsx | 38 +++++++++--------- .../articles/components/TagSelect/index.tsx | 38 +++++++++--------- .../projects/components/TagSelect/index.tsx | 38 +++++++++--------- 5 files changed, 88 insertions(+), 87 deletions(-) diff --git a/src/pages/account/settings/index.tsx b/src/pages/account/settings/index.tsx index 3b0a3402..fc59fbf2 100644 --- a/src/pages/account/settings/index.tsx +++ b/src/pages/account/settings/index.tsx @@ -1,6 +1,6 @@ import { GridContent } from '@ant-design/pro-components'; import { Menu } from 'antd'; -import React, { useLayoutEffect, useRef, useState } from 'react'; +import React, { useCallback, useLayoutEffect, useRef, useState } from 'react'; import BaseView from './components/base'; import BindingView from './components/binding'; import NotificationView from './components/notification'; @@ -25,7 +25,8 @@ const Settings: React.FC = () => { selectKey: 'base', }); const dom = useRef(null); - const resize = () => { + + const resize = useCallback(() => { requestAnimationFrame(() => { if (!dom.current) { return; @@ -38,21 +39,20 @@ const Settings: React.FC = () => { if (window.innerWidth < 768 && offsetWidth > 400) { mode = 'horizontal'; } - setInitConfig({ - ...initConfig, + setInitConfig((prev) => ({ + ...prev, mode: mode as SettingsState['mode'], - }); + })); }); - }; + }, []); + useLayoutEffect(() => { - if (dom.current) { - window.addEventListener('resize', resize); - resize(); - } + window.addEventListener('resize', resize); + resize(); return () => { window.removeEventListener('resize', resize); }; - }, []); + }, [resize]); const getMenu = () => { return Object.keys(menuMap).map((item) => ({ key: item, diff --git a/src/pages/dashboard/monitor/components/ActiveChart/index.tsx b/src/pages/dashboard/monitor/components/ActiveChart/index.tsx index 006807d4..1a6197de 100644 --- a/src/pages/dashboard/monitor/components/ActiveChart/index.tsx +++ b/src/pages/dashboard/monitor/components/ActiveChart/index.tsx @@ -1,6 +1,6 @@ import { Area } from '@ant-design/plots'; import { Statistic } from 'antd'; -import { useCallback, useEffect, useRef, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import useStyles from './index.style'; function fixedZero(val: number) { @@ -19,27 +19,31 @@ function getActiveData() { const ActiveChart = () => { const timerRef = useRef(null); - const requestRef = useRef(null); const { styles } = useStyles(); const [activeData, setActiveData] = useState<{ x: string; y: number }[]>([]); - const loopData = useCallback(() => { - requestRef.current = requestAnimationFrame(() => { - timerRef.current = window.setTimeout(() => { - setActiveData(getActiveData()); - loopData(); - }, 2000); - }); - }, []); useEffect(() => { + const loopData = () => { + setActiveData(getActiveData()); + timerRef.current = window.setTimeout(loopData, 2000); + }; loopData(); return () => { - clearTimeout(timerRef.current as number); - if (requestRef.current) { - cancelAnimationFrame(requestRef.current); + if (timerRef.current) { + clearTimeout(timerRef.current); } }; - }, [loopData]); + }, []); + + // Memoize max and median to avoid double sort on every render + const { maxValue, medianValue } = useMemo(() => { + if (!activeData.length) return { maxValue: 0, medianValue: 0 }; + const sorted = [...activeData].sort((a, b) => a.y - b.y); + return { + maxValue: sorted[sorted.length - 1]?.y ?? 0, + medianValue: sorted[Math.floor(sorted.length / 2)]?.y ?? 0, + }; + }, [activeData]); return (
@@ -65,11 +69,8 @@ const ActiveChart = () => { {activeData && (
-

{[...activeData].sort()[activeData.length - 1]?.y + 200} 亿元

-

- {[...activeData].sort()[Math.floor(activeData.length / 2)]?.y}{' '} - 亿元 -

+

{maxValue + 200} 亿元

+

{medianValue} 亿元

diff --git a/src/pages/list/search/applications/components/TagSelect/index.tsx b/src/pages/list/search/applications/components/TagSelect/index.tsx index 100067ee..80107cd2 100644 --- a/src/pages/list/search/applications/components/TagSelect/index.tsx +++ b/src/pages/list/search/applications/components/TagSelect/index.tsx @@ -2,7 +2,7 @@ import { DownOutlined, UpOutlined } from '@ant-design/icons'; import { useMergedState } from '@rc-component/util'; import { Tag } from 'antd'; import classNames from 'classnames'; -import React, { type FC, useState } from 'react'; +import React, { type FC, useMemo, useState } from 'react'; import useStyles from './index.style'; const { CheckableTag } = Tag; @@ -75,33 +75,33 @@ const TagSelect: FC & { node?.type && (node.type.isTagSelectOption || node.type.displayName === 'TagSelectOption'); - const getAllTags = () => { + + // Memoize all tags to avoid recalculating on every render + const allTags = useMemo(() => { const childrenArray = React.Children.toArray( children, ) as TagSelectOptionElement[]; - const checkedTags = childrenArray + return childrenArray .filter((child) => isTagSelectOption(child)) .map((child) => child.props.value); - return checkedTags || []; - }; + }, [children]); + + // Use Set for O(1) lookups + const valueSet = useMemo(() => new Set(value || []), [value]); + const onSelectAll = (checked: boolean) => { - let checkedTags: (string | number)[] = []; - if (checked) { - checkedTags = getAllTags(); - } - setValue(checkedTags); + setValue(checked ? [...allTags] : []); }; const handleTagChange = (tag: string | number, checked: boolean) => { - const checkedTags: (string | number)[] = [...(value || [])]; - const index = checkedTags.indexOf(tag); - if (checked && index === -1) { - checkedTags.push(tag); - } else if (!checked && index > -1) { - checkedTags.splice(index, 1); + const checkedTags = new Set(value || []); + if (checked) { + checkedTags.add(tag); + } else { + checkedTags.delete(tag); } - setValue(checkedTags); + setValue([...checkedTags]); }; - const checkedAll = getAllTags().length === value?.length; + const checkedAll = allTags.length === value?.length && allTags.length > 0; const { expandText = '展开', collapseText = '收起', @@ -128,7 +128,7 @@ const TagSelect: FC & { return React.cloneElement(child, { key: `tag-select-${child.props.value}`, value: child.props.value, - checked: value && value.indexOf(child.props.value) > -1, + checked: valueSet.has(child.props.value), onChange: handleTagChange, }); } diff --git a/src/pages/list/search/articles/components/TagSelect/index.tsx b/src/pages/list/search/articles/components/TagSelect/index.tsx index 100067ee..80107cd2 100644 --- a/src/pages/list/search/articles/components/TagSelect/index.tsx +++ b/src/pages/list/search/articles/components/TagSelect/index.tsx @@ -2,7 +2,7 @@ import { DownOutlined, UpOutlined } from '@ant-design/icons'; import { useMergedState } from '@rc-component/util'; import { Tag } from 'antd'; import classNames from 'classnames'; -import React, { type FC, useState } from 'react'; +import React, { type FC, useMemo, useState } from 'react'; import useStyles from './index.style'; const { CheckableTag } = Tag; @@ -75,33 +75,33 @@ const TagSelect: FC & { node?.type && (node.type.isTagSelectOption || node.type.displayName === 'TagSelectOption'); - const getAllTags = () => { + + // Memoize all tags to avoid recalculating on every render + const allTags = useMemo(() => { const childrenArray = React.Children.toArray( children, ) as TagSelectOptionElement[]; - const checkedTags = childrenArray + return childrenArray .filter((child) => isTagSelectOption(child)) .map((child) => child.props.value); - return checkedTags || []; - }; + }, [children]); + + // Use Set for O(1) lookups + const valueSet = useMemo(() => new Set(value || []), [value]); + const onSelectAll = (checked: boolean) => { - let checkedTags: (string | number)[] = []; - if (checked) { - checkedTags = getAllTags(); - } - setValue(checkedTags); + setValue(checked ? [...allTags] : []); }; const handleTagChange = (tag: string | number, checked: boolean) => { - const checkedTags: (string | number)[] = [...(value || [])]; - const index = checkedTags.indexOf(tag); - if (checked && index === -1) { - checkedTags.push(tag); - } else if (!checked && index > -1) { - checkedTags.splice(index, 1); + const checkedTags = new Set(value || []); + if (checked) { + checkedTags.add(tag); + } else { + checkedTags.delete(tag); } - setValue(checkedTags); + setValue([...checkedTags]); }; - const checkedAll = getAllTags().length === value?.length; + const checkedAll = allTags.length === value?.length && allTags.length > 0; const { expandText = '展开', collapseText = '收起', @@ -128,7 +128,7 @@ const TagSelect: FC & { return React.cloneElement(child, { key: `tag-select-${child.props.value}`, value: child.props.value, - checked: value && value.indexOf(child.props.value) > -1, + checked: valueSet.has(child.props.value), onChange: handleTagChange, }); } diff --git a/src/pages/list/search/projects/components/TagSelect/index.tsx b/src/pages/list/search/projects/components/TagSelect/index.tsx index 100067ee..80107cd2 100644 --- a/src/pages/list/search/projects/components/TagSelect/index.tsx +++ b/src/pages/list/search/projects/components/TagSelect/index.tsx @@ -2,7 +2,7 @@ import { DownOutlined, UpOutlined } from '@ant-design/icons'; import { useMergedState } from '@rc-component/util'; import { Tag } from 'antd'; import classNames from 'classnames'; -import React, { type FC, useState } from 'react'; +import React, { type FC, useMemo, useState } from 'react'; import useStyles from './index.style'; const { CheckableTag } = Tag; @@ -75,33 +75,33 @@ const TagSelect: FC & { node?.type && (node.type.isTagSelectOption || node.type.displayName === 'TagSelectOption'); - const getAllTags = () => { + + // Memoize all tags to avoid recalculating on every render + const allTags = useMemo(() => { const childrenArray = React.Children.toArray( children, ) as TagSelectOptionElement[]; - const checkedTags = childrenArray + return childrenArray .filter((child) => isTagSelectOption(child)) .map((child) => child.props.value); - return checkedTags || []; - }; + }, [children]); + + // Use Set for O(1) lookups + const valueSet = useMemo(() => new Set(value || []), [value]); + const onSelectAll = (checked: boolean) => { - let checkedTags: (string | number)[] = []; - if (checked) { - checkedTags = getAllTags(); - } - setValue(checkedTags); + setValue(checked ? [...allTags] : []); }; const handleTagChange = (tag: string | number, checked: boolean) => { - const checkedTags: (string | number)[] = [...(value || [])]; - const index = checkedTags.indexOf(tag); - if (checked && index === -1) { - checkedTags.push(tag); - } else if (!checked && index > -1) { - checkedTags.splice(index, 1); + const checkedTags = new Set(value || []); + if (checked) { + checkedTags.add(tag); + } else { + checkedTags.delete(tag); } - setValue(checkedTags); + setValue([...checkedTags]); }; - const checkedAll = getAllTags().length === value?.length; + const checkedAll = allTags.length === value?.length && allTags.length > 0; const { expandText = '展开', collapseText = '收起', @@ -128,7 +128,7 @@ const TagSelect: FC & { return React.cloneElement(child, { key: `tag-select-${child.props.value}`, value: child.props.value, - checked: value && value.indexOf(child.props.value) > -1, + checked: valueSet.has(child.props.value), onChange: handleTagChange, }); }