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

React.memo结合Framer Motion与Next.js Image组件的问题解决

问题解决方案

1. 修复hero为null时的图片渲染异常

next/image的src属性不允许为空字符串,当hero为null时传递空src会触发渲染错误;同时仅通过visibility: hidden隐藏图片仍会触发Image组件的加载逻辑,造成资源浪费。修改方案如下:

在HeroImage组件中,仅当hero存在时才渲染Image组件,否则返回空占位容器:

const HeroImage = React.memo(
  ({ hero, index }: { hero: Hero | null; index: number }) => {
    if (!hero) return <div className="relative w-full h-full" />;

    return (
      <motion.div
        className="relative w-full h-full"
        initial={{ opacity: 0, x: -20 }}
        animate={{ opacity: 1, x: 0 }}
        exit={{ opacity: 0, x: 20 }}
        transition={{ duration: 0.3 }}
      >
        <Image
          src={hero.img}
          alt={hero.name}
          layout="fill"
          objectFit="cover"
          draggable={false}
        />
      </motion.div>
    );
  },
  // 自定义memo比较逻辑,避免对象引用变化导致的无效重渲染
  (prevProps, nextProps) => {
    if (prevProps.hero === nextProps.hero) return true;
    return prevProps.hero?.name === nextProps.hero?.name && prevProps.hero?.img === nextProps.hero?.img;
  }
);

2. 解决切换英雄时所有图片触发动画的问题

核心原因

  • 默认React.memo的浅比较无法准确判断hero对象的内容变化,导致不必要的组件重渲染
  • framer-motion的initial动画默认在组件每次更新时触发,而非仅当hero从null变为有效数据时

修改方案

  1. 自定义React.memo比较函数:精确对比hero的name和img属性,避免因对象引用变化触发重渲染
  2. 控制动画触发时机:通过状态跟踪hero的变化,仅当hero从null变为有效数据时触发入场动画

完整修改后的代码:

import { motion } from "framer-motion";
import Image from "next/image";
import React, { useEffect, useState } from "react";

interface Hero {
  name: string;
  img: string;
}

const HeroImage = React.memo(
  ({ hero, index }: { hero: Hero | null; index: number }) => {
    const [shouldAnimate, setShouldAnimate] = useState(false);

    useEffect(() => {
      // 仅当hero从null变为有效数据时触发动画
      if (hero) {
        setShouldAnimate(true);
      } else {
        setShouldAnimate(false);
      }
    }, [hero]);

    if (!hero) return <div className="relative w-full h-full" />;

    return (
      <motion.div
        className="relative w-full h-full"
        initial={{ opacity: 0, x: -20 }}
        animate={shouldAnimate ? { opacity: 1, x: 0 } : { opacity: 1, x: 0 }}
        exit={{ opacity: 0, x: 20 }}
        transition={{ duration: 0.3 }}
        onExitComplete={() => setShouldAnimate(false)}
      >
        <Image
          src={hero.img}
          alt={hero.name}
          layout="fill"
          objectFit="cover"
          draggable={false}
        />
      </motion.div>
    );
  },
  (prevProps, nextProps) => {
    if (prevProps.hero === nextProps.hero) return true;
    if (!prevProps.hero && !nextProps.hero) return true;
    return prevProps.hero?.name === nextProps.hero?.name && prevProps.hero?.img === nextProps.hero?.img;
  }
);

const HeroCard = React.memo(
  ({ hero, index }: { hero: Hero | null; index: number }) => {
    return (
      <div
        className="relative overflow-hidden rounded flex-grow bg-cover"
        style={{
          backgroundImage: `url('/placeholder.png')`,
        }}
      >
        <HeroImage hero={hero} index={index} />
        <div className="absolute bottom-0 left-0 right-0 bg-black/70 text-white text-center py-1">
          {hero?.name || "未选择英雄"}
        </div>
      </div>
    );
  },
  (prevProps, nextProps) => {
    if (prevProps.hero === nextProps.hero) return true;
    if (!prevProps.hero && !nextProps.hero) return true;
    return prevProps.hero?.name === nextProps.hero?.name && prevProps.hero?.img === nextProps.hero?.img;
  }
);

export { HeroImage, HeroCard };

额外优化建议

  • 父组件渲染HeroCard列表时,使用hero?.name或唯一ID作为key(而非index),避免列表重排引发的无效渲染:
    // 父组件示例
    {heroList.map((hero, idx) => (
      <HeroCard key={hero?.name || `hero-card-${idx}`} hero={hero} index={idx} />
    ))}
    
  • 移除layoutId属性(除非需要跨组件布局动画),该属性会让framer-motion额外跟踪元素布局,可能引发不必要的重渲染

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 09:25:16