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

Next.js中_app.tsx内useInView钩子路由切换不执行问题求助

自定义useInView钩子路由切换失效问题解决

问题概述

使用自定义useInView钩子实现组件进入视口时的动画效果,遇到以下问题:

  • 将钩子放在_app.tsx中,路由切换时钩子不执行,但_app.tsx内的console.log()正常运行;仅手动刷新页面时,钩子才正常工作,动画能流畅触发。
  • 尝试在页面共用组件中使用该钩子,虽能运行但加载有延迟,动画不够流畅。认为_app.tsx是合适的放置位置,但不确定是否最优。

最终目标:让所有动画流畅运行,组件进入视口时触发,且路由切换到新页面时重新触发动画。

相关代码

_app.tsx

import type { AppProps } from 'next/app'
import { ThemeProvider } from 'styled-components'
import GlobalStyle from '../components/GlobalStyles'
import { useInView } from '../hooks/useInView'
import { theme } from '../theme/theme'

export default function App({ Component, pageProps }: AppProps) {
  useInView()
  return (
    <ThemeProvider theme={theme}>
      <GlobalStyle />
      <Component {...pageProps} />
    </ThemeProvider>
  )
}

useInView 钩子

import { useEffect } from 'react'

export const useInView = () => {
  useEffect(() => {
    function inView() {
      const observer = new IntersectionObserver(
        (intersections) => {
          intersections.forEach(({ target, isIntersecting }) => {
            if (isIntersecting) {
              target.classList.add('in-view')
            } else {
              target.classList.remove('in-view')
            }
          })
        },
        {
          threshold: 0,
        }
      )

      document.querySelectorAll('.scroll-spy').forEach((element) => {
        observer.observe(element)
      })
    }
    inView()
  }, [])
}

问题原因

  1. 路由切换不触发钩子:useInView内的useEffect依赖为空数组,仅在_app.tsx首次挂载时执行一次。Next.js客户端路由切换时,_app.tsx不会重新挂载,因此钩子不会再次运行,新页面的.scroll-spy元素无法被监听。
  2. 共用组件使用有延迟:共用组件的挂载时机晚于页面内容渲染,导致监听逻辑启动滞后,动画触发不流畅。

解决方法

方案1:监听路由变化,重新执行监听逻辑(推荐)

修改useInView钩子,利用Next.js的useRouter监听路由切换事件,每次路由完成后重新监听新页面元素,同时清理旧的Observer避免内存泄漏:

import { useEffect } from 'react'
import { useRouter } from 'next/router'

export const useInView = () => {
  const router = useRouter()

  useEffect(() => {
    let observer: IntersectionObserver | null = null

    const setupObserver = () => {
      // 清理旧监听,避免内存泄漏
      if (observer) {
        observer.disconnect()
      }

      observer = new IntersectionObserver(
        (intersections) => {
          intersections.forEach(({ target, isIntersecting }) => {
            isIntersecting 
              ? target.classList.add('in-view') 
              : target.classList.remove('in-view')
          })
        },
        { threshold: 0 }
      )

      // 监听当前页面所有.scroll-spy元素
      document.querySelectorAll('.scroll-spy').forEach(element => {
        observer!.observe(element)
      })
    }

    // 首次挂载执行监听
    setupObserver()

    // 路由切换完成后重新初始化监听
    const handleRouteChange = () => {
      // 微任务确保新页面DOM已渲染完成
      setTimeout(setupObserver, 0)
    }

    router.events.on('routeChangeComplete', handleRouteChange)

    // 组件卸载时清理事件监听和Observer
    return () => {
      router.events.off('routeChangeComplete', handleRouteChange)
      observer?.disconnect()
    }
  }, [router])
}

方案2:封装页面级监听组件

若不想全局监听,可封装ScrollSpyWrapper组件,在每个页面中包裹内容,页面挂载时自动监听,卸载时清理:

// components/ScrollSpyWrapper.tsx
import { useEffect, useRef } from 'react'

export const ScrollSpyWrapper = ({ children }: { children: React.ReactNode }) => {
  const observerRef = useRef<IntersectionObserver | null>(null)

  useEffect(() => {
    observerRef.current = new IntersectionObserver(
      (intersections) => {
        intersections.forEach(({ target, isIntersecting }) => {
          isIntersecting 
            ? target.classList.add('in-view') 
            : target.classList.remove('in-view')
        })
      },
      { threshold: 0 }
    )

    document.querySelectorAll('.scroll-spy').forEach(el => {
      observerRef.current!.observe(el)
    })

    return () => {
      observerRef.current?.disconnect()
    }
  }, [])

  return <>{children}</>
}

在页面中使用:

// pages/ExamplePage.tsx
import { ScrollSpyWrapper } from '../components/ScrollSpyWrapper'

export default function ExamplePage() {
  return (
    <ScrollSpyWrapper>
      <div className="scroll-spy">需要动画的元素</div>
      {/* 其他页面内容 */}
    </ScrollSpyWrapper>
  )
}

方案选择建议

方案1更适配你的需求:将钩子保留在_app.tsx中,配合路由监听既能全局生效,又能在路由切换时及时更新监听,不会出现动画延迟问题,同时清理逻辑能保证性能。

内容的提问来源于stack exchange,提问作者Nicole Lopez

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.31 03:18:24