Next.js 13.x.x集成Framer-motion遇hydration不匹配错误求助
Next.js 13 SSR + Framer Motion 消除
data-projection-id Hydration 警告 问题原因
控制台出现的Warning: Prop 'data-projection-id' did not match警告,是因为Framer Motion在客户端渲染时会自动生成data-projection-id属性用于动画状态跟踪,但服务端渲染时该属性值为null,导致React在hydrate过程中检测到服务端与客户端的DOM属性不匹配。
解决方案
方案1:添加suppressHydrationWarning属性
直接给所有motion元素添加suppressHydrationWarning属性,告诉React忽略该内部属性的hydrate不匹配(该ID仅用于Framer Motion内部逻辑,不影响页面功能)。
修改后的组件代码:
'use client'; import { motion } from 'framer-motion'; import Vitamin from '@/components/vitamin'; const Fruit = ({ fruit }) => { const vitCardContainer = { hidden: { opacity: 0 }, visible: { opacity: 1, transition: { delayChildren: 0.125, staggerChildren: 0.05, }, }, }; const vitCardItems = { hidden: { opacity: 0, scale: 0.8 }, visible: { opacity: 1, scale: 1, transition: { duration: 0.25 } }, }; return ( <section> <h1 className='text-2xl font-bold text-red-600'>{fruit!.name}</h1> <div className='flex flex-col my-4'> <h3 className='text-xl font-bold text-red-400'>Overview</h3> <p className='my-2 text-base font-semibold text-red-400'> {fruit!.overview} </p> </div> <div className='flex flex-col my-4'> <h3 className='text-xl font-bold text-red-400'>Vitamins Present</h3> <motion.ul suppressHydrationWarning className={ 'flex flex-row flex-wrap my-4 ' + (fruit!.vitamins?.length > 3 ? 'justify-between' : 'justify-normal') } variants={vitCardContainer} initial='hidden' animate='visible' > {fruit!.vitamins?.slice(0, 6).map((c) => ( <motion.li suppressHydrationWarning key={'fruit-vitamins-' + c.id} variants={vitCardItems} className={fruit!.vitamins?.length > 3 ? '' : 'pr-4'} > <Vitamin id={c.id} name={c.name} character={c.character} profile_path={c.profile_path} /> </motion.li> ))} </motion.ul> </div> </section> ); }; export default Fruit;
方案2:延迟动画初始化到客户端挂载后
通过useAnimation控制器结合useEffect,确保动画仅在客户端hydrate完成后启动,从根源避免服务端与客户端的DOM状态差异。
修改后的组件代码:
'use client'; import { motion, useAnimation } from 'framer-motion'; import { useEffect } from 'react'; import Vitamin from '@/components/vitamin'; const Fruit = ({ fruit }) => { const animationControls = useAnimation(); const vitCardContainer = { hidden: { opacity: 0 }, visible: { opacity: 1, transition: { delayChildren: 0.125, staggerChildren: 0.05, }, }, }; const vitCardItems = { hidden: { opacity: 0, scale: 0.8 }, visible: { opacity: 1, scale: 1, transition: { duration: 0.25 } }, }; // 客户端挂载完成后启动动画 useEffect(() => { animationControls.start('visible'); }, [animationControls]); return ( <section> <h1 className='text-2xl font-bold text-red-600'>{fruit!.name}</h1> <div className='flex flex-col my-4'> <h3 className='text-xl font-bold text-red-400'>Overview</h3> <p className='my-2 text-base font-semibold text-red-400'> {fruit!.overview} </p> </div> <div className='flex flex-col my-4'> <h3 className='text-xl font-bold text-red-400'>Vitamins Present</h3> <motion.ul className={ 'flex flex-row flex-wrap my-4 ' + (fruit!.vitamins?.length > 3 ? 'justify-between' : 'justify-normal') } variants={vitCardContainer} initial='hidden' animate={animationControls} > {fruit!.vitamins?.slice(0, 6).map((c) => ( <motion.li key={'fruit-vitamins-' + c.id} variants={vitCardItems} className={fruit!.vitamins?.length > 3 ? '' : 'pr-4'} > <Vitamin id={c.id} name={c.name} character={c.character} profile_path={c.profile_path} /> </motion.li> ))} </motion.ul> </div> </section> ); }; export default Fruit;
方案选择
- 方案1实现简单,适合快速消除警告;
- 方案2更贴合SSR的hydration机制,避免潜在的DOM不一致问题,同时保留完整动画效果。
内容的提问来源于stack exchange,提问作者Dibyendu Saha
相关产品推荐
相关产品推荐

