You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React ProgressBar组件因父组件状态切换无端重渲染,memo不生效?

问题:React ProgressBar组件因父组件重渲染重复执行动画

我在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.03 09:55:24