React+Framer Motion折叠侧边栏中如何阻止静态图标组件重复渲染?
React+Framer Motion折叠侧边栏中如何阻止静态图标组件重复渲染?
刚接触React遇到这种“明明没改props却还是重渲染”的问题真的很闹心,我来帮你拆解下原因和可行的解决办法~
先搞清楚为什么会出现这种情况
你已经用了React.memo和useMemo来缓存图标,但还是出现重渲染,大概率是这几个细节没注意到:
- 传递JSX元素而非组件引用:你之前直接把
<CalendarPlus {...props} />作为button_icon传递,哪怕用了useMemo缓存,一旦父组件(SidebarDiv)因为is_expanded切换而重渲染,还是有可能因为隐式依赖导致JSX元素的引用变化,React.memo的浅比较会误以为props变了。 - SidebarItem内部的隐式动态依赖:你代码里的
motion.button用了is_page来控制颜色,但这个is_page是组件内部隐式获取的(比如用usePathname判断),如果这个值在切换侧边栏时意外变化,会触发motion.button的动画更新,连带里面的图标一起“被重渲染”。 - 父组件的motion props不稳定:包裹SidebarItem的
motion.div的item_icon_motion_props如果是每次渲染都重新创建的对象,会导致父组件重渲染,进而触发子组件SidebarItem的重渲染。
具体解决步骤
1. 把图标从JSX元素改成组件类型传递
不要直接传渲染好的<CalendarPlus />,而是传组件本身,这样引用绝对稳定,React.memo能准确识别:
// SidebarItem.tsx import { memo } from 'react'; import { useRouter } from 'next/router'; import { motion } from 'framer-motion'; // 重新定义props,让button_icon接收组件类型而非JSX元素 type ButtonProps = { buttonIcon: React.ComponentType<{ size?: number; className?: string; color?: string }>; buttonRoute: string; buttonText: string; isExpanded: boolean; isCurrentPage: boolean; // 把原来的is_page改成显式props传递 }; const SidebarItem = memo(function SidebarItem({ buttonIcon: ButtonIcon, // 重命名为大写,符合组件命名规范 buttonRoute, buttonText, isExpanded, isCurrentPage, }: ButtonProps) { const router = useRouter(); return ( <motion.button animate={{ color: isCurrentPage ? '#698f3f' : '#384f1f' }} transition={{ duration: 0 }} onClick={() => router.push(`./${buttonRoute}`)} > {/* 在这里渲染图标组件,传递静态props */} <ButtonIcon size={30} className="p-0.5 ml-0.5 my-1" color="currentColor" /> {isExpanded ? ( <motion.span initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ duration: 0.4 }} > {buttonText} </motion.span> ) : null} </motion.button> ); }); export default SidebarItem;
2. 父组件传递图标组件并缓存动态props
在SidebarDiv里,直接传CalendarPlus组件,同时用useMemo缓存item_icon_motion_props,避免父组件不必要的重渲染:
// SidebarDiv.tsx import { useMemo } from 'react'; import { usePathname } from 'next/navigation'; import { motion } from 'framer-motion'; import SidebarItem from './SidebarItem'; import { CalendarPlus } from 'your-icon-library'; export default function SidebarDiv({ isExpanded }) { const pathname = usePathname(); // 缓存motion组件的props,只有依赖变化时才更新 const item_icon_motion_props = useMemo(() => ({ // 这里写你的motion配置,比如 animate: { x: isExpanded ? 0 : -20 } 之类的 }), [isExpanded]); // 显式计算当前页面状态,传递给SidebarItem const isTailLinkPage = pathname === '/taillink'; return ( <ul className="flex-1 px-3"> <motion.div {...item_icon_motion_props}> <SidebarItem buttonIcon={CalendarPlus} isExpanded={isExpanded} buttonRoute="/taillink" buttonText="TailLink" isCurrentPage={isTailLinkPage} /> </motion.div> </ul> ); }
3. 给图标组件本身加上memo(可选但推荐)
如果你的CalendarPlus是自定义组件,记得用React.memo包裹它,双重保险:
// 比如你的CalendarPlus组件 import { memo } from 'react'; const CalendarPlus = memo((props) => { console.log('CalendarPlus渲染了'); // 可以用这个日志测试是否还重渲染 return <svg {...props}>{/* 图标内容 */}</svg>; }); export default CalendarPlus;
最后验证
做完这些修改后,你可以在图标组件里加个console.log,切换侧边栏展开/收起时,应该不会再打印日志了,图标也不会有可见的重渲染闪烁。
内容来源于stack exchange
相关产品推荐
相关产品推荐

