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() }, []) }
问题原因
- 路由切换不触发钩子:
useInView内的useEffect依赖为空数组,仅在_app.tsx首次挂载时执行一次。Next.js客户端路由切换时,_app.tsx不会重新挂载,因此钩子不会再次运行,新页面的.scroll-spy元素无法被监听。 - 共用组件使用有延迟:共用组件的挂载时机晚于页面内容渲染,导致监听逻辑启动滞后,动画触发不流畅。
解决方法
方案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
相关产品推荐
相关产品推荐

