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 { 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<HTMLDivElement>(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,

39
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<number | null>(null);
const requestRef = useRef<number | null>(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 (
<div className={styles.activeChart}>
@ -65,11 +69,8 @@ const ActiveChart = () => {
{activeData && (
<div>
<div className={styles.activeChartGrid}>
<p>{[...activeData].sort()[activeData.length - 1]?.y + 200} 亿</p>
<p>
{[...activeData].sort()[Math.floor(activeData.length / 2)]?.y}{' '}
亿
</p>
<p>{maxValue + 200} 亿</p>
<p>{medianValue} 亿</p>
</div>
<div className={styles.dashedLine}>
<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 { 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<TagSelectProps> & {
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<TagSelectProps> & {
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,
});
}

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 { 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<TagSelectProps> & {
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<TagSelectProps> & {
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,
});
}

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 { 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<TagSelectProps> & {
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<TagSelectProps> & {
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,
});
}

Loading…
Cancel
Save