React ProgressBar组件因父组件状态切换无端重渲染,memo不生效?
我在React项目中开发了一个ProgressBar组件,每次挂载时会通过useEffect执行“填充”动画。但组件树上层的抽屉组件切换开合状态时会触发父组件状态变更,导致ProgressBar被重渲染,动画随之重复执行,我想阻止这种情况。
我知道props没变化,可以用memo包裹组件避免重渲染,而且memo默认做浅比较,所以自定义了比较函数,但console.log("check?")始终没执行,怀疑areEqual函数没运行、memo没生效。请问怎么解决抽屉切换时的无端重渲染?
我尝试的自定义比较函数:
const areEqual = (prev: Readonly<ProgressBarProps>, cur: Readonly<ProgressBarProps>) => { console.log("check?"); if (typeof prev.fillType === "string" && typeof cur.fillType === "string") { return prev.fillType === cur.fillType; } else if ( typeof prev.fillType !== "string" && "from" in prev.fillType && typeof cur.fillType !== "string" && "from" in cur.fillType ) { return prev.fillType.from === cur.fillType.from && prev.fillType.to === cur.fillType.to; } else { return false; } } const MemoizedProgressBar = memo(ProgressBar, areEqual); export default MemoizedProgressBar;
ProgressBar完整代码:
import { Box, SxProps } from "@mui/material"; import { memo, useEffect, useRef } from "react"; type monotone = string; type gradient = { from: string; to: string; }; type ProgressBarProps = { fillType: monotone | gradient; }; const ProgressBar = ({ fillType }: ProgressBarProps) => { const progressFillRef = useRef<HTMLDivElement>(); useEffect(() => { if (progressFillRef !== null) { setTimeout(() => { progressFillRef.current!.style.width = "50%"; }, 1000); } }, []); const progressFillStyles = { height: "100%", transition: "width 0.5s cubic-bezier(.26,.8,.68,1.07)", borderRadius: "inherit", width: 0, } satisfies SxProps; const getFillType = () => { if (typeof fillType === "string") { return { backgroundColor: fillType, ...progressFillStyles }; } return "from" in fillType ? { backgroundImage: `linear-gradient(to left top, ${fillType.from}, ${fillType.to})`, ...progressFillStyles, } : { backgroundColor: "green", ...progressFillStyles }; }; return ( <Box sx={{ width: "100%", maxWidth: "700px", height: "15px", backgroundColor: "#ddd", borderRadius: "10px", }} > <Box ref={progressFillRef} sx={getFillType()} /> </Box> ); }; const areEqual = (prev: Readonly<ProgressBarProps>, cur: Readonly<ProgressBarProps>) => { console.log("check?"); if (typeof prev.fillType === "string" && typeof cur.fillType === "string") { return prev.fillType === cur.fillType; } else if ( typeof prev.fillType !== "string" && "from" in prev.fillType && typeof cur.fillType !== "string" && "from" in cur.fillType ) { return prev.fillType.from === cur.fillType.from && prev.fillType.to === cur.fillType.to; } else { return false; } } const MemoizedProgressBar = memo(ProgressBar, areEqual); export default MemoizedProgressBar;
解决方案
1. 为什么areEqual没触发?
memo的自定义比较函数只有在父组件传递的props发生浅变化时才会执行。如果父组件每次重渲染时,传递给ProgressBar的fillType是新创建的对象(比如直接写fillType={{from: '#xxx', to: '#yyy'}}),React的浅比较会认为props已变更,直接触发重渲染,根本不会调用你自定义的areEqual——这就是你console.log没输出的核心原因。
2. 核心修复步骤
方案一:稳定props引用(最优解)
确保父组件传递的fillType引用不变:
- 如果是对象类型,把它声明为父组件的常量,或者用
useMemo缓存:
// 父组件中 const fillConfig = useMemo(() => ({from: '#ff0000', to: '#00ff00'}), []); // 渲染ProgressBar时 <MemoizedProgressBar fillType={fillConfig} />
这样父组件重渲染时,fillConfig的引用不会改变,React的浅比较直接通过,memo生效,甚至可能不需要自定义areEqual函数。
方案二:修正自定义areEqual逻辑
如果必须动态生成fillType,可以简化并优化比较函数逻辑,确保类型判断更严谨:
const areEqual = (prev: Readonly<ProgressBarProps>, cur: Readonly<ProgressBarProps>) => { console.log("check?"); // 先判断类型是否一致 if (typeof prev.fillType !== typeof cur.fillType) return false; // 字符串类型直接比较值 if (typeof prev.fillType === 'string') { return prev.fillType === cur.fillType; } // 对象类型比较内部属性值 return prev.fillType.from === cur.fillType.from && prev.fillType.to === cur.fillType.to; };
3. 额外优化:避免动画重复执行
即使阻止了重渲染,也可以给动画加状态控制,确保只在组件挂载时执行一次:
const ProgressBar = ({ fillType }: ProgressBarProps) => { const progressFillRef = useRef<HTMLDivElement>(null); const [hasAnimated, setHasAnimated] = useState(false); useEffect(() => { if (!hasAnimated && progressFillRef.current) { const timer = setTimeout(() => { progressFillRef.current!.style.width = "50%"; setHasAnimated(true); }, 1000); return () => clearTimeout(timer); } }, [hasAnimated]); // ...其余代码不变 };
这样即使组件意外重渲染,动画也只会执行一次。
4. 验证memo是否生效
可以在ProgressBar组件内部加日志,查看是否在抽屉切换时触发渲染:
const ProgressBar = ({ fillType }: ProgressBarProps) => { console.log("ProgressBar渲染了"); // ...其余代码 };
如果抽屉切换时该日志不输出,说明memo已经生效。
内容的提问来源于stack exchange,提问作者CM K

