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

Next.js页面中如何用useEffect实现环形进度条同步动画

Next.js 中环形进度条与数字同步增长的 useEffect 改造方案

问题根源

Next.js 采用服务端渲染(SSR)机制,页面刷新时组件尚未完成客户端 hydration,直接调用 document.querySelector 会无法获取到目标 DOM 元素,导致环形进度条和数字递增动画失效。

改造方案

通过 React 的 useRef + useEffect 组合,确保 DOM 挂载完成后再执行动画逻辑,同时同步进度条与数字的增长节奏:

完整组件代码示例

import { useEffect, useRef } from 'react';

const Widgets = () => {
  // 用 useRef 保存 DOM 引用,避免重复查询
  const progressCircleRef = useRef(null);
  const numberDisplayRef = useRef(null);

  // 目标进度值(可根据业务需求调整)
  const targetProgress = 85;

  useEffect(() => {
    // 组件挂载完成后,确保 DOM 已存在
    const progressCircle = progressCircleRef.current;
    const numberDisplay = numberDisplayRef.current;

    if (!progressCircle || !numberDisplay) return;

    // 初始化进度条参数
    const radius = progressCircle.r.baseVal.value;
    const circumference = 2 * Math.PI * radius;
    progressCircle.style.strokeDasharray = `${circumference}`;
    progressCircle.style.strokeDashoffset = `${circumference}`;

    let currentProgress = 0;
    let animationFrameId;

    // 同步更新进度条与数字的动画函数
    const updateAnimation = () => {
      if (currentProgress >= targetProgress) {
        cancelAnimationFrame(animationFrameId);
        return;
      }

      currentProgress += 1;
      // 计算进度条的偏移量
      const offset = circumference - (currentProgress / 100) * circumference;
      progressCircle.style.strokeDashoffset = offset;
      // 更新显示数字
      numberDisplay.textContent = `${currentProgress}%`;

      animationFrameId = requestAnimationFrame(updateAnimation);
    };

    // 启动动画
    animationFrameId = requestAnimationFrame(updateAnimation);

    // 组件卸载时清理动画帧,避免内存泄漏
    return () => {
      cancelAnimationFrame(animationFrameId);
    };
  }, [targetProgress]); // 依赖目标进度值,值变化时重新触发动画

  return (
    <div className="widget-container">
      <svg className="progress-ring" width="120" height="120">
        <circle
          ref={progressCircleRef}
          className="progress-ring-circle"
          stroke="#e0e0e0"
          strokeWidth="8"
          fill="transparent"
          r="46"
          cx="60"
          cy="60"
        />
        <circle
          className="progress-ring-progress"
          stroke="#4CAF50"
          strokeWidth="8"
          strokeLinecap="round"
          fill="transparent"
          r="46"
          cx="60"
          cy="60"
        />
      </svg>
      <span ref={numberDisplayRef} className="progress-number">0%</span>
    </div>
  );
};

export default Widgets;

关键细节说明

  • useRef 替代 document.querySelector:直接绑定到目标DOM元素,确保每次渲染都能拿到正确的节点,避免SSR阶段的DOM查询问题。
  • useEffect 执行时机:依赖数组仅包含目标进度值,确保动画逻辑只在组件客户端挂载完成后执行,此时DOM已完全渲染。
  • 同步动画逻辑:通过 requestAnimationFrame 实现流畅的帧动画,每帧同时更新进度条的 stroke-dashoffset 属性和数字显示,保证两者同步增长。
  • 清理函数:组件卸载时取消动画帧,防止内存泄漏。

内容的提问来源于stack exchange,提问作者iamundertaker

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.14 22:40:37