Next.js 13基于屏幕尺寸设置framer-motion初始动画值时的Hydration错误
在Next.js 13旧版pages目录中使用framer-motion实现视口滑入动画时,因大屏/移动端不同初始X值设置,出现服务端与客户端渲染样式不匹配的Hydration错误,具体提示:
Warning: Prop
styledid not match. Server: "transform:translateX(86vw) translateZ(0)" Client: "transform:translateX(93vw) translateZ(0)"
Uncaught Error: Hydration failed because the initial UI does not match what was rendered on the server.
问题根源
服务端渲染时不存在window对象,你的isMobile()函数会返回false;而客户端如果处于移动端环境,函数返回true,导致初始transform样式在服务端和客户端不一致。另外需要明确:'use client'在pages目录中不会让组件完全跳过服务端渲染,它仅标记组件需要客户端交互能力,服务端仍会先渲染一次静态内容,因此仍会出现样式不匹配。
解决方案
方案1:useEffect+useState延迟客户端初始值设置
通过useState初始化服务端默认值,再在useEffect(仅客户端执行)中根据屏幕尺寸更新初始X值,避免服务端与客户端渲染差异:
'use client'; import { useState, useEffect } from 'react'; import { motion } from 'framer-motion'; import { isMobile } from '../path/to/your/utils'; export default function AnimatedHeading() { const [initialX, setInitialX] = useState('86vw'); // 服务端默认渲染值 useEffect(() => { // 仅在客户端运行,根据屏幕尺寸更新初始位置 setInitialX(isMobile() ? '93vw' : '86vw'); }, []); return ( <motion.h1 initial={{ x: initialX }} whileInView={{ x: 0 }} transition={{ duration: 0.75, type: 'spring' }} viewport={{ once: true }} className={classes.element} > Example content </motion.h1> ); }
方案2:使用framer-motion内置useMediaQuery钩子
framer-motion的useMediaQuery钩子会安全地在客户端获取媒体查询结果,服务端默认返回false,客户端加载后自动更新,避免Hydration错误:
'use client'; import { motion, useMediaQuery } from 'framer-motion'; export default function AnimatedHeading() { const isMobile = useMediaQuery('(min-width: 320px) and (max-width: 767.98px)'); return ( <motion.h1 initial={{ x: isMobile ? '93vw' : '86vw' }} whileInView={{ x: 0 }} transition={{ duration: 0.75, type: 'spring' }} viewport={{ once: true }} className={classes.element} > Example content </motion.h1> ); }
方案3:动态导入并禁用SSR
通过Next.js的dynamic工具导入组件并禁用服务端渲染,让组件完全在客户端加载,从根源避免渲染差异:
// 页面组件中 import dynamic from 'next/dynamic'; const AnimatedHeading = dynamic(() => import('../components/AnimatedHeading'), { ssr: false, loading: () => <h1>Loading...</h1> // 可选加载态 }); export default function HomePage() { return ( <div> <AnimatedHeading /> </div> ); }
组件文件中正常编写动画逻辑:
// AnimatedHeading.jsx 'use client'; import { motion } from 'framer-motion'; import { isMobile } from '../path/to/your/utils'; export default function AnimatedHeading() { return ( <motion.h1 initial={{ x: isMobile() ? '93vw' : '86vw' }} whileInView={{ x: 0 }} transition={{ duration: 0.75, type: 'spring' }} viewport={{ once: true }} className={classes.element} > Example content </motion.h1> ); }
内容的提问来源于stack exchange,提问作者FlushBG

