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

Next.js中motion的whileInView在软导航时未触发的问题求助

Next.js中motion的whileInView在软导航时未触发的问题求助

嘿,我明白你遇到的这个困扰了——用Next.js做作品集时,项目卡片的whileInView动画在硬刷新时正常,但软导航到「更多项目」页面就完全没反应,这确实挺影响体验的。我来帮你分析下问题根源和几个可行的解决办法:

问题根源

在Next.js的客户端软导航(路由切换不刷新整个页面)过程中,组件可能会被复用或者挂载后,motion的视口监听逻辑没有重新初始化。尤其是你用了自定义的root: scrollRef作为视口根元素,软导航后这个ref的绑定可能没有被正确更新,导致motion检测不到元素进入视口的事件;另外如果你的<Projects />组件在多个页面复用,Next.js的组件复用机制也会让动画状态保留,不会重新触发。

解决办法

1. 给复用组件加唯一Key,强制重新挂载

如果你的Home页面和Projects页面都用了同一个<Projects />组件,给它加一个基于当前路径的Key,确保路由切换时组件完全重新挂载,动画自然会重新触发:

// 在Home和Projects页面中
import { usePathname } from "next/navigation";

const Home = () => {
  const pathname = usePathname();
  return (
    // 其他页面内容
    <Projects key={pathname} />
  );
};

const ProjectsPage = () => {
  const pathname = usePathname();
  return (
    // 其他页面内容
    <Projects key={pathname} />
  );
};

2. 移除自定义Root,使用默认视口

试试去掉viewport配置里的root: scrollRef,改用浏览器默认的视口(window)来检测,这样软导航后motion的监听逻辑会更稳定:

// 修改project-card.tsx中每个motion元素的viewport配置
viewport={{
  margin: skipMargin ? undefined : "-100px",
  once: true
}}

3. 监听路由变化,手动重置动画状态

利用Next.js的路由钩子监听路径变化,每次切换路由时重置动画的触发状态,让motion重新检测视口:

"use client";
import { cn } from "@/lib/utils";
import { motion } from "motion/react";
import Image from "next/image";
import { useRef, useState, useEffect } from "react";
import { usePathname } from "next/navigation";

export const ProjectCard = ({
  reverse,
  imgUrl,
  title,
  skipMargin
}: {
  title: string;
  reverse?: boolean;
  imgUrl: string;
  skipMargin?: boolean;
}) => {
  const scrollRef = useRef<HTMLDivElement>(null);
  const pathname = usePathname();
  const [isReadyToAnimate, setIsReadyToAnimate] = useState(false);

  // 路由变化时重置动画状态
  useEffect(() => {
    setIsReadyToAnimate(false);
    // 延迟100ms确保组件更新完成后再触发检测
    const timer = setTimeout(() => setIsReadyToAnimate(true), 100);
    return () => clearTimeout(timer);
  }, [pathname]);

  return (
    <article
      ref={scrollRef}
      className="max-sm:space-y-4 overflow-hidden sm:grid sm:grid-cols-10 gap-4"
    >
      <motion.div
        initial={isReadyToAnimate ? {
          translateX: reverse ? "-20px" : "20px",
          opacity: 0
        } : {}}
        whileInView={{ translateX: "0px", opacity: 1 }}
        viewport={{
          root: scrollRef,
          margin: skipMargin ? undefined : "-100px",
          once: true
        }}
        transition={{ ease: "easeInOut", duration: 0.5, delay: 0.1 }}
      >
        {/* 卡片文字内容 */}
      </motion.div>
      <motion.div
        initial={isReadyToAnimate ? {
          translateX: reverse ? "20px" : "-20px",
          opacity: 0
        } : {}}
        whileInView={{ translateX: "0px", opacity: 1 }}
        viewport={{
          root: scrollRef,
          margin: skipMargin ? undefined : "-100px",
          once: true
        }}
        transition={{ ease: "easeInOut", duration: 0.5, delay: 0.1 }}
      >
        <Image src={imgUrl} alt={title} fill className="object-cover" />
      </motion.div>
    </article>
  );
};

4. 手动用useInView钩子控制动画

改用motion的useInView钩子手动检测元素是否在视口,主动控制动画触发时机,避免自动监听的兼容性问题:

"use client";
import { cn } from "@/lib/utils";
import { motion, useInView } from "motion/react";
import Image from "next/image";
import { useRef } from "react";

export const ProjectCard = ({
  reverse,
  imgUrl,
  title,
  skipMargin
}: {
  title: string;
  reverse?: boolean;
  imgUrl: string;
  skipMargin?: boolean;
}) => {
  const scrollRef = useRef<HTMLDivElement>(null);
  // 手动检测元素是否进入视口
  const isInView = useInView(scrollRef, {
    margin: skipMargin ? undefined : "-100px",
    once: true
  });

  return (
    <article
      ref={scrollRef}
      className="max-sm:space-y-4 overflow-hidden sm:grid sm:grid-cols-10 gap-4"
    >
      <motion.div
        initial={{ translateX: reverse ? "-20px" : "20px", opacity: 0 }}
        animate={isInView ? { translateX: "0px", opacity: 1 } : {}}
        transition={{ ease: "easeInOut", duration: 0.5, delay: 0.1 }}
      >
        {/* 卡片文字内容 */}
      </motion.div>
      <motion.div
        initial={{ translateX: reverse ? "20px" : "-20px", opacity: 0 }}
        animate={isInView ? { translateX: "0px", opacity: 1 } : {}}
        transition={{ ease: "easeInOut", duration: 0.5, delay: 0.1 }}
      >
        <Image src={imgUrl} alt={title} fill className="object-cover" />
      </motion.div>
    </article>
  );
};

你可以先试试前两种方法,操作起来最简单,应该能快速解决问题。如果还是不行,再尝试第三种或第四种方案。

备注:内容来源于stack exchange,提问作者zain ul din

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.14 11:13:10