Browse Source

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 <noreply@anthropic.com>
pull/11631/head
afc163 2 weeks ago
parent
commit
28e98b2334
  1. 22
      src/pages/account/settings/index.tsx
  2. 39
      src/pages/dashboard/monitor/components/ActiveChart/index.tsx
  3. 38
      src/pages/list/search/applications/components/TagSelect/index.tsx
  4. 38
      src/pages/list/search/articles/components/TagSelect/index.tsx
  5. 38
      src/pages/list/search/projects/components/TagSelect/index.tsx

22
src/pages/account/settings/index.tsx

@ -1,6 +1,6 @@
import { GridContent } from '@ant-design/pro-components'; import { GridContent } from '@ant-design/pro-components';
import { Menu } from 'antd'; 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 BaseView from './components/base';
import BindingView from './components/binding'; import BindingView from './components/binding';
import NotificationView from './components/notification'; import NotificationView from './components/notification';
@ -25,7 +25,8 @@ const Settings: React.FC = () => {
selectKey: 'base', selectKey: 'base',
}); });
const dom = useRef<HTMLDivElement>(null); const dom = useRef<HTMLDivElement>(null);
const resize = () => {
const resize = useCallback(() => {
requestAnimationFrame(() => { requestAnimationFrame(() => {
if (!dom.current) { if (!dom.current) {
return; return;
@ -38,21 +39,20 @@ const Settings: React.FC = () => {
if (window.innerWidth < 768 && offsetWidth > 400) { if (window.innerWidth < 768 && offsetWidth > 400) {
mode = 'horizontal'; mode = 'horizontal';
} }
setInitConfig({ setInitConfig((prev) => ({
...initConfig, ...prev,
mode: mode as SettingsState['mode'], mode: mode as SettingsState['mode'],
}); }));
}); });
}; }, []);
useLayoutEffect(() => { useLayoutEffect(() => {
if (dom.current) { window.addEventListener('resize', resize);
window.addEventListener('resize', resize); resize();
resize();
}
return () => { return () => {
window.removeEventListener('resize', resize); window.removeEventListener('resize', resize);
}; };
}, []); }, [resize]);
const getMenu = () => { const getMenu = () => {
return Object.keys(menuMap).map((item) => ({ return Object.keys(menuMap).map((item) => ({
key: item, key: item,

39
src/pages/dashboard/monitor/components/ActiveChart/index.tsx

@ -1,6 +1,6 @@
import { Area } from '@ant-design/plots'; import { Area } from '@ant-design/plots';
import { Statistic } from 'antd'; import { Statistic } from 'antd';
import { useCallback, useEffect, useRef, useState } from 'react'; import { useEffect, useMemo, useRef, useState } from 'react';
import useStyles from './index.style'; import useStyles from './index.style';
function fixedZero(val: number) { function fixedZero(val: number) {
@ -19,27 +19,31 @@ function getActiveData() {
const ActiveChart = () => { const ActiveChart = () => {
const timerRef = useRef<number | null>(null); const timerRef = useRef<number | null>(null);
const requestRef = useRef<number | null>(null);
const { styles } = useStyles(); const { styles } = useStyles();
const [activeData, setActiveData] = useState<{ x: string; y: number }[]>([]); const [activeData, setActiveData] = useState<{ x: string; y: number }[]>([]);
const loopData = useCallback(() => {
requestRef.current = requestAnimationFrame(() => {
timerRef.current = window.setTimeout(() => {
setActiveData(getActiveData());
loopData();
}, 2000);
});
}, []);
useEffect(() => { useEffect(() => {
const loopData = () => {
setActiveData(getActiveData());
timerRef.current = window.setTimeout(loopData, 2000);
};
loopData(); loopData();
return () => { return () => {
clearTimeout(timerRef.current as number); if (timerRef.current) {
if (requestRef.current) { clearTimeout(timerRef.current);
cancelAnimationFrame(requestRef.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 ( return (
<div className={styles.activeChart}> <div className={styles.activeChart}>
@ -65,11 +69,8 @@ const ActiveChart = () => {
{activeData && ( {activeData && (
<div> <div>
<div className={styles.activeChartGrid}> <div className={styles.activeChartGrid}>
<p>{[...activeData].sort()[activeData.length - 1]?.y + 200} 亿</p> <p>{maxValue + 200} 亿</p>
<p> <p>{medianValue} 亿</p>
{[...activeData].sort()[Math.floor(activeData.length / 2)]?.y}{' '}
亿
</p>
</div> </div>
<div className={styles.dashedLine}> <div className={styles.dashedLine}>
<div className={styles.line} /> <div className={styles.line} />

38
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 { useMergedState } from '@rc-component/util';
import { Tag } from 'antd'; import { Tag } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import React, { type FC, useState } from 'react'; import React, { type FC, useMemo, useState } from 'react';
import useStyles from './index.style'; import useStyles from './index.style';
const { CheckableTag } = Tag; const { CheckableTag } = Tag;
@ -75,33 +75,33 @@ const TagSelect: FC<TagSelectProps> & {
node?.type && node?.type &&
(node.type.isTagSelectOption || (node.type.isTagSelectOption ||
node.type.displayName === 'TagSelectOption'); node.type.displayName === 'TagSelectOption');
const getAllTags = () => {
// Memoize all tags to avoid recalculating on every render
const allTags = useMemo(() => {
const childrenArray = React.Children.toArray( const childrenArray = React.Children.toArray(
children, children,
) as TagSelectOptionElement[]; ) as TagSelectOptionElement[];
const checkedTags = childrenArray return childrenArray
.filter((child) => isTagSelectOption(child)) .filter((child) => isTagSelectOption(child))
.map((child) => child.props.value); .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) => { const onSelectAll = (checked: boolean) => {
let checkedTags: (string | number)[] = []; setValue(checked ? [...allTags] : []);
if (checked) {
checkedTags = getAllTags();
}
setValue(checkedTags);
}; };
const handleTagChange = (tag: string | number, checked: boolean) => { const handleTagChange = (tag: string | number, checked: boolean) => {
const checkedTags: (string | number)[] = [...(value || [])]; const checkedTags = new Set(value || []);
const index = checkedTags.indexOf(tag); if (checked) {
if (checked && index === -1) { checkedTags.add(tag);
checkedTags.push(tag); } else {
} else if (!checked && index > -1) { checkedTags.delete(tag);
checkedTags.splice(index, 1);
} }
setValue(checkedTags); setValue([...checkedTags]);
}; };
const checkedAll = getAllTags().length === value?.length; const checkedAll = allTags.length === value?.length && allTags.length > 0;
const { const {
expandText = '展开', expandText = '展开',
collapseText = '收起', collapseText = '收起',
@ -128,7 +128,7 @@ const TagSelect: FC<TagSelectProps> & {
return React.cloneElement(child, { return React.cloneElement(child, {
key: `tag-select-${child.props.value}`, key: `tag-select-${child.props.value}`,
value: child.props.value, value: child.props.value,
checked: value && value.indexOf(child.props.value) > -1, checked: valueSet.has(child.props.value),
onChange: handleTagChange, onChange: handleTagChange,
}); });
} }

38
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 { useMergedState } from '@rc-component/util';
import { Tag } from 'antd'; import { Tag } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import React, { type FC, useState } from 'react'; import React, { type FC, useMemo, useState } from 'react';
import useStyles from './index.style'; import useStyles from './index.style';
const { CheckableTag } = Tag; const { CheckableTag } = Tag;
@ -75,33 +75,33 @@ const TagSelect: FC<TagSelectProps> & {
node?.type && node?.type &&
(node.type.isTagSelectOption || (node.type.isTagSelectOption ||
node.type.displayName === 'TagSelectOption'); node.type.displayName === 'TagSelectOption');
const getAllTags = () => {
// Memoize all tags to avoid recalculating on every render
const allTags = useMemo(() => {
const childrenArray = React.Children.toArray( const childrenArray = React.Children.toArray(
children, children,
) as TagSelectOptionElement[]; ) as TagSelectOptionElement[];
const checkedTags = childrenArray return childrenArray
.filter((child) => isTagSelectOption(child)) .filter((child) => isTagSelectOption(child))
.map((child) => child.props.value); .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) => { const onSelectAll = (checked: boolean) => {
let checkedTags: (string | number)[] = []; setValue(checked ? [...allTags] : []);
if (checked) {
checkedTags = getAllTags();
}
setValue(checkedTags);
}; };
const handleTagChange = (tag: string | number, checked: boolean) => { const handleTagChange = (tag: string | number, checked: boolean) => {
const checkedTags: (string | number)[] = [...(value || [])]; const checkedTags = new Set(value || []);
const index = checkedTags.indexOf(tag); if (checked) {
if (checked && index === -1) { checkedTags.add(tag);
checkedTags.push(tag); } else {
} else if (!checked && index > -1) { checkedTags.delete(tag);
checkedTags.splice(index, 1);
} }
setValue(checkedTags); setValue([...checkedTags]);
}; };
const checkedAll = getAllTags().length === value?.length; const checkedAll = allTags.length === value?.length && allTags.length > 0;
const { const {
expandText = '展开', expandText = '展开',
collapseText = '收起', collapseText = '收起',
@ -128,7 +128,7 @@ const TagSelect: FC<TagSelectProps> & {
return React.cloneElement(child, { return React.cloneElement(child, {
key: `tag-select-${child.props.value}`, key: `tag-select-${child.props.value}`,
value: child.props.value, value: child.props.value,
checked: value && value.indexOf(child.props.value) > -1, checked: valueSet.has(child.props.value),
onChange: handleTagChange, onChange: handleTagChange,
}); });
} }

38
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 { useMergedState } from '@rc-component/util';
import { Tag } from 'antd'; import { Tag } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import React, { type FC, useState } from 'react'; import React, { type FC, useMemo, useState } from 'react';
import useStyles from './index.style'; import useStyles from './index.style';
const { CheckableTag } = Tag; const { CheckableTag } = Tag;
@ -75,33 +75,33 @@ const TagSelect: FC<TagSelectProps> & {
node?.type && node?.type &&
(node.type.isTagSelectOption || (node.type.isTagSelectOption ||
node.type.displayName === 'TagSelectOption'); node.type.displayName === 'TagSelectOption');
const getAllTags = () => {
// Memoize all tags to avoid recalculating on every render
const allTags = useMemo(() => {
const childrenArray = React.Children.toArray( const childrenArray = React.Children.toArray(
children, children,
) as TagSelectOptionElement[]; ) as TagSelectOptionElement[];
const checkedTags = childrenArray return childrenArray
.filter((child) => isTagSelectOption(child)) .filter((child) => isTagSelectOption(child))
.map((child) => child.props.value); .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) => { const onSelectAll = (checked: boolean) => {
let checkedTags: (string | number)[] = []; setValue(checked ? [...allTags] : []);
if (checked) {
checkedTags = getAllTags();
}
setValue(checkedTags);
}; };
const handleTagChange = (tag: string | number, checked: boolean) => { const handleTagChange = (tag: string | number, checked: boolean) => {
const checkedTags: (string | number)[] = [...(value || [])]; const checkedTags = new Set(value || []);
const index = checkedTags.indexOf(tag); if (checked) {
if (checked && index === -1) { checkedTags.add(tag);
checkedTags.push(tag); } else {
} else if (!checked && index > -1) { checkedTags.delete(tag);
checkedTags.splice(index, 1);
} }
setValue(checkedTags); setValue([...checkedTags]);
}; };
const checkedAll = getAllTags().length === value?.length; const checkedAll = allTags.length === value?.length && allTags.length > 0;
const { const {
expandText = '展开', expandText = '展开',
collapseText = '收起', collapseText = '收起',
@ -128,7 +128,7 @@ const TagSelect: FC<TagSelectProps> & {
return React.cloneElement(child, { return React.cloneElement(child, {
key: `tag-select-${child.props.value}`, key: `tag-select-${child.props.value}`,
value: child.props.value, value: child.props.value,
checked: value && value.indexOf(child.props.value) > -1, checked: valueSet.has(child.props.value),
onChange: handleTagChange, onChange: handleTagChange,
}); });
} }

Loading…
Cancel
Save