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} <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被错误共享:
React.createRef()每次组件渲染都会生成新的ref对象,在严格模式的双重渲染检测中,Framer Motion的滚动监听逻辑可能错误复用了这些ref的状态- 即使改用
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
相关产品推荐
相关产品推荐

