推荐阅读
'use client'
import { motion, AnimatePresence } from 'framer-motion'
import { usePathname } from 'next/navigation'
const defaultProps = {
initial: { opacity: 0 },
animate: { opacity: 1 },
exit: { opacity: 0 },
transition: { ease: 'easeInOut', duration: 0.5 },
}
export function PageTransitionEffect({
children,
}: { children: React.ReactNode }) {
const pathname = usePathname()
return (
<AnimatePresence mode='popLayout'>
<motion.main {...defaultProps} key={pathname}>
{children}
</motion.main>
</AnimatePresence>
)
}
然而这样是不行的,旧页面在执行卸载动画期间,内容会变成新页面的内容。
我们需要确保,旧页面的内容在执行卸载动画期间,内容不变。
此时就需要用到 LayoutRouterContext
:
import { LayoutRouterContext } from 'next/dist/shared/lib/app-router-context.shared-runtime'
import { useContext, useRef } from 'react'
function FrozenRouter(props: { children: React.ReactNode }) {
const context = useContext(LayoutRouterContext ?? {})
const frozen = useRef(context).current
if (!frozen) {
return <>{props.children}</>
}
return (
<LayoutRouterContext.Provider value={frozen}>
{props.children}
</LayoutRouterContext.Provider>
)
}
用这个 FrozenRouter
包裹一下上面关键代码中的 children
就好了。
以防我们在什么地方需要访问当前动画是否已完成,所以添加一个全局钩子
// usePageTransitionEffect.ts
import { create } from 'zustand'
export const usePageTransitionEffect = create(() => ({
exited: true,
}))
接下来是完整代码:
// PageTransitionEffect.tsx
'use client'
import { usePageTransitionEffect } from './usePageTransitionEffect'
import { motion, AnimatePresence } from 'framer-motion'
import { usePathname } from 'next/navigation'
import { LayoutRouterContext } from 'next/dist/shared/lib/app-router-context.shared-runtime'
import { useContext, useEffect, useRef } from 'react'
import type { MotionProps } from 'framer-motion'
function FrozenRouter(props: { children: React.ReactNode }) {
const context = useContext(LayoutRouterContext ?? {})
const frozen = useRef(context).current
if (!frozen) {
return <>{props.children}</>
}
return (
<LayoutRouterContext.Provider value={frozen}>
{props.children}
</LayoutRouterContext.Provider>
)
}
export type PageTransitionEffectConfig = Omit<MotionProps, 'children' | 'key'>
export type PageTransitionEffectProps = PageTransitionEffectConfig & {
children: React.ReactNode
}
const defaultProps = {
initial: { opacity: 0 },
animate: { opacity: 1 },
exit: { opacity: 0 },
transition: { ease: 'easeInOut', duration: 0.5 },
} satisfies PageTransitionEffectConfig
const onEnter = () => {
usePageTransitionEffect.setState({
exited: false,
})
}
const onExitComplete = () => {
usePageTransitionEffect.setState({
exited: true,
})
}
export function PageTransitionEffect({
children,
...props
}: PageTransitionEffectProps) {
const pathname = usePathname()
useEffect(() => {
onEnter()
}, [pathname])
return (
<AnimatePresence mode='popLayout' onExitComplete={onExitComplete}>
<motion.main {...defaultProps} {...props} key={pathname}>
<FrozenRouter>{children}</FrozenRouter>
</motion.main>
</AnimatePresence>
)
}
需要动画的地方用 PageTransitionEffect
包裹一下就可以了。
// layout.tsx
export default function CustomLayout({ children }) {
return <>
<YourHeader />
{/* 此时,页面切换时,只有 content 有动画,header 和 footer 没有动画 */}
<PageTransitionEffect style={{ width: '100%', maxWidth: 1024, margin: '0 auto' }}>
{children}
</PageTransitionEffect>
<YourFooter />
</>
}
// children component
export function ChildrenComponent() {
const { exited } = usePageTransitionEffect()
// ...
}
如非特别声明,本站作品均为原创,遵循【自由转载-保持署名-非商用-非衍生 创意共享 3.0 许可证】。
对于转载作品,如需二次转载,请遵循原作许可。