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

Next.js开发与生产环境中React Refs行为差异问题排查

问题描述

我跟着教程开发滚动驱动的动画效果,已经实现了滚动线条和闭合圆形的预期功能,但部署到生产环境后,所有圆形似乎共用同一个ref,滚动时同步动画而非独立完成。这个问题仅在生产构建中出现,开发环境一切正常。

补充说明:已创建项目复现该问题,能看到开发模式与生产模式的行为差异;也有沙箱展示两种模式的区别。

现象对比

  • 开发环境:每个圆形会根据对应组件的滚动位置独立完成动画
  • 生产环境:所有圆形同步变化,动画完全一致
核心代码

ExperienceDetail.tsx

import React, { useRef } from 'react';
import { Job } from './jobList';
import styles from './ExperienceDetail.module.css';
import ListIcon from './ListIcon';
import { motion } from 'framer-motion';

interface DetailsProps {
  job: Job;
}

const ExperienceDetail: React.FC<DetailsProps> = ({ job }) => {
  const ref = React.createRef<HTMLElement>();
  return (
    <li
      ref={ref as any}
      className={`${styles.detailContainer}`}
      id={job.position}
    >
      <ListIcon reference={ref} />
      <motion.div
        initial={{ y: 50 }}
        whileInView={{ y: 0 }}
        transition={{ duration: 0.618, type: 'spring' }}
      >
        <h3 className={`${styles.title} is-capitalized has-text-weight-bold `}>
          {job.position}&nbsp;
          <a
            href={job.companyLink}
            target="_blank"
            className="has-text-primary-dark is-capitalized "
          >
            @{job.company}
          </a>
        </h3>
        <span className="is-capitalized has-text-weight-medium has-text-grey">
          {job.time} | {job.location}
        </span>
        <p
          className={'mt-2 has-text-weight-medium'}
          dangerouslySetInnerHTML={{ __html: job.description }}
        />
      </motion.div>
    </li>
  );
};

export default ExperienceDetail;

ListIcon.tsx

import React from 'react';
import styles from './ListIcon.module.css';
import { motion, useScroll } from 'framer-motion';

interface ListIconProps {
  reference: React.MutableRefObject<HTMLElement | null>;
}

const ListIcon: React.FC<ListIconProps> = ({ reference }) => {
  const { scrollYProgress } = useScroll({
    target: reference,
    offset: ['center end', 'center center'],
  });
  return (
    <figure className={styles.listIcon}>
      <svg width="75" height="75" viewBox="0 0 100 100">
        <circle cx="75" cy="50" r="20" className={styles.circle1} />
        <motion.circle
          cx="75"
          cy="50"
          r="20"
          className={styles.circle2}
          style={{ pathLength: scrollYProgress }}
        />
        <circle
          cx="75"
          cy="50"
          r="10"
          className={`${styles.circle3} ${styles.pulse}`}
        />
      </svg>
    </figure>
  );
};

export default ListIcon;

父组件渲染代码

<ul>
    {list.map((item, index) => (
        <ExperienceDetail job={item} key={`${type}-${index}`} />
    ))}
</ul>

我已经尝试过取消map直接渲染组件,也试过把useRef换成React.createRef,但都没解决问题。请问为什么开发和生产环境会有这种行为差异?


原因与解决方案

核心原因

生产环境下Next.js默认开启严格模式,再加上Framer Motion的useScroll钩子在生产优化时的缓存逻辑,导致多个组件实例的ref被错误共享:

  1. React.createRef()每次组件渲染都会生成新的ref对象,在严格模式的双重渲染检测中,Framer Motion的滚动监听逻辑可能错误复用了这些ref的状态
  2. 即使改用useRef,如果没有明确区分不同实例的滚动监听,生产环境的打包优化也可能导致状态共享

解决方案

方案1:改用useRef替代createRef

useRef在组件整个生命周期内只会生成一个固定的ref对象,能避免生产环境下的ref复用问题:

// 修改ExperienceDetail.tsx中的ref创建逻辑
const ExperienceDetail: React.FC<DetailsProps> = ({ job }) => {
  const ref = useRef<HTMLElement>(null); // 替换createRef为useRef
  return (
    <li
      ref={ref}
      className={`${styles.detailContainer}`}
      id={job.position}
    >
      {/* 其余代码保持不变 */}
    </li>
  );
};

方案2:给useScroll添加唯一标识

给每个useScroll实例添加唯一的scope属性,强制区分不同组件的滚动监听:

// 先修改ListIcon的props定义
interface ListIconProps {
  reference: React.MutableRefObject<HTMLElement | null>;
  uniqueKey: string;
}

// 再修改useScroll配置
const ListIcon: React.FC<ListIconProps> = ({ reference, uniqueKey }) => {
  const { scrollYProgress } = useScroll({
    target: reference,
    offset: ['center end', 'center center'],
    scope: uniqueKey // 添加唯一标识
  });
  // 其余代码保持不变
};

// 在ExperienceDetail中传入唯一标识
<ListIcon reference={ref} uniqueKey={job.position} />

方案3:临时禁用严格模式(不推荐)

如果以上方案无效,可以临时在next.config.js中关闭严格模式,但会失去React严格模式的副作用检测能力:

// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: false,
};

module.exports = nextConfig;

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.02 02:35:55