如何使用 Framer Motion 和 Next.js 14 进行页面转换?

Ale*_*iva 5 reactjs next.js framer-motion react-server-components

我正在尝试使用 Next.js v14 进行页面转换,但没有成功。

没有显示错误,动画只是不起作用。我猜这是因为它layout.tsx是在服务器上呈现的。但我该如何解决呢?

我的layout.tsx代码(根级别):

import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import Sidebar from "@/components/Sidebar";
import Breadcrumbs from "@/components/Breadcrumbs";
import Topbar from "@/components/Topbar";
import PageTransitionEffect from "@/components/PageTransitionEffect";

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body className={`${inter.className} flex flex-col min-h-screen`}>
        <Topbar />

        <div className="bg-gray-800 text-gray-200 flex flex-grow overflow-x-hidden px-0">
          <Sidebar />

          <div className="flex-col mx-auto flex w-full py-6 px-4 sm:px-6 md:px-14">
            <Breadcrumbs />

            <PageTransitionEffect>{children}</PageTransitionEffect>
          </div>
        </div>
      </body>
    </html>
  );
}
Run Code Online (Sandbox Code Playgroud)

我的PageTransitionEffect组件代码:

"use client";

import { motion } from "framer-motion";

const PageTransitionEffect = ({ children }: { children: React.ReactNode }) => {
  const variants = {
    hidden: { opacity: 0, x: -200, y: 0 },
    enter: { opacity: 1, x: 0, y: 0 },
    exit: { opacity: 0, x: 0, y: -100 },
  };

  return (
    <motion.div
      initial="hidden"
      animate="enter"
      exit="exit"
      variants={variants}
      transition={{ type: "linear" }}
    >
      {children}
    </motion.div>
  );
};

export default PageTransitionEffect;
Run Code Online (Sandbox Code Playgroud)

编辑#1(警告消息):

Warning: Detected multiple renderers concurrently rendering the same context provider. This is currently unsupported.
    at FrozenRouter (webpack-internal:///(ssr)/./src/components/PageTransitionEffect/index.tsx:21:70)
    at div
    at MotionComponent (webpack-internal:///(ssr)/./node_modules/framer-motion/dist/es/motion/index.mjs:49:65)
    at PopChildMeasure (webpack-internal:///(ssr)/./node_modules/framer-motion/dist/es/components/AnimatePresence/PopChild.mjs:13:1)
    at PopChild (webpack-internal:///(ssr)/./node_modules/framer-motion/dist/es/components/AnimatePresence/PopChild.mjs:33:21)
    at PresenceChild (webpack-internal:///(ssr)/./node_modules/framer-motion/dist/es/components/AnimatePresence/PresenceChild.mjs:15:26)
    at AnimatePresence (webpack-internal:///(ssr)/./node_modules/framer-motion/dist/es/components/AnimatePresence/index.mjs:72:28)
    at PageTransitionEffect (webpack-internal:///(ssr)/./src/components/PageTransitionEffect/index.tsx:49:33)
    at div
    at div
    at body
    at html
    at RootLayout (webpack-internal:///(ssr)/./src/app/layout.tsx:21:23)
    at Lazy
    at RedirectErrorBoundary (webpack-internal:///(ssr)/./node_modules/next/dist/client/components/redirect-boundary.js:71:9)
    at RedirectBoundary (webpack-internal:///(ssr)/./node_modules/next/dist/client/components/redirect-boundary.js:79:11)
    at ReactDevOverlay (webpack-internal:///(ssr)/./node_modules/next/dist/client/components/react-dev-overlay/internal/ReactDevOverlay.js:66:9)
    at HotReload (webpack-internal:///(ssr)/./node_modules/next/dist/client/components/react-dev-overlay/hot-reloader-client.js:298:11)
    at Router (webpack-internal:///(ssr)/./node_modules/next/dist/client/components/app-router.js:154:11)
    at ErrorBoundaryHandler (webpack-internal:///(ssr)/./node_modules/next/dist/client/components/error-boundary.js:99:9)
    at ErrorBoundary (webpack-internal:///(ssr)/./node_modules/next/dist/client/components/error-boundary.js:128:11)
    at AppRouter (webpack-internal:///(ssr)/./node_modules/next/dist/client/components/app-router.js:426:13)
    at Lazy
    at Lazy
    at C:\Projects\node_modules\next\dist\compiled\next-server\app-page.runtime.dev.js:35:374733
    at C:\Projects\node_modules\next\dist\compiled\next-server\app-page.runtime.dev.js:35:374733
    at ServerInsertedHTMLProvider (C:\Projects\node_modules\next\dist\compiled\next-server\app-page.runtime.dev.js:38:23140)
Run Code Online (Sandbox Code Playgroud)

组件代码:

"use client";

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, useRef } from "react";

function FrozenRouter(props: { children: React.ReactNode }) {
  const context = useContext(LayoutRouterContext);
  const frozen = useRef(context).current;

  return (
    <LayoutRouterContext.Provider value={frozen}>
      {props.children}
    </LayoutRouterContext.Provider>
  );
}

const variants = {
  hidden: { opacity: 0, x: -30, y: 0 },
  enter: { opacity: 1, x: 0, y: 0 },
  exit: { opacity: 0, x: -30, y: 0 },
};

const PageTransitionEffect = ({ children }: { children: React.ReactNode }) => {
  // The `key` is tied to the url using the `usePathname` hook.
  const key = usePathname();

  return (
    <AnimatePresence mode="popLayout">
      <motion.div
        key={key}
        initial="hidden"
        animate="enter"
        exit="exit"
        variants={variants}
        transition={{ type: "linear", duration: 0.2, delay: 0.1 }}
      >
        <FrozenRouter>{children}</FrozenRouter>
      </motion.div>
    </AnimatePresence>
  );
};

export default PageTransitionEffect;
Run Code Online (Sandbox Code Playgroud)

Ric*_*ico 13

目前存在一个由应用程序路由器和共享布局引起的错误,导致该错误无法正常工作。不过,有一个解决方法。除了解决方法之外,我们还需要 3 个其他东西:

  1. AnimatePresence来自成帧器运动(这有助于我们使用退出和进入动画)
  2. key运动元素上的A触发重新渲染 (这会在页面更改时触发动画)
  3. 添加mode='popLayout'AnimatePresence (这会立即从树中删除旧页面。您可以使用模式值,不同的模式对于某些动画更好。)

例子:

"use client";

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, useRef } from "react";

function FrozenRouter(props: { children: React.ReactNode }) {
  const context = useContext(LayoutRouterContext ?? {});
  const frozen = useRef(context).current;

  return (
    <LayoutRouterContext.Provider value={frozen}>
      {props.children}
    </LayoutRouterContext.Provider>
  );
}

const variants = {
  hidden: { opacity: 0, x: -200, y: 100 },
  enter: { opacity: 1, x: 0, y: 0 },
  exit: { opacity: 0, x: 0, y: -100 },
};

const PageTransitionEffect = ({ children }: { children: React.ReactNode }) => {
  // The `key` is tied to the url using the `usePathname` hook.
  const key = usePathname();

  return (
    <AnimatePresence mode="popLayout">
      <motion.div
        key={key}
        initial="hidden"
        animate="enter"
        exit="exit"
        variants={variants}
        transition={{ type: "linear" }}
        className="overflow-hidden"
      >
        <FrozenRouter>{children}</FrozenRouter>
      </motion.div>
    </AnimatePresence>
  );
};

export default PageTransitionEffect;
Run Code Online (Sandbox Code Playgroud)

这是一个工作示例。