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

Framer Motion结合React Portals使用时过渡动画未触发问题求助

问题原因

你遇到的动画不触发问题有两个核心原因:

  1. Context中事件监听的处理函数引用不一致,导致点击按钮时会重复触发状态更新,打断Framer Motion的动画执行流程
  2. 当使用React Portal挂载组件时,AnimatePresence需要明确感知子元素的挂载/卸载周期,你当前把条件渲染逻辑放在AnimatePresence内部,首次添加子元素时Framer Motion没有正确触发状态切换,导致元素停留在initial的opacity:0状态

修复步骤

1. 修复事件监听的引用问题

Context.js中的useEffect内,添加和移除的事件监听函数需要保持同一个引用,避免重复绑定:

// Context.js 修改后的useEffect部分
React.useEffect(() => {
  const startEl = document.getElementById("start-portal");
  // 把处理函数提取成单独变量,保证add和remove用的是同一个引用
  const handleClick = () => setActiveItem(0);
  startEl.addEventListener("click", handleClick);
  return () => {
    startEl.removeEventListener("click", handleClick);
  };
}, []);

2. 调整AnimatePresence和条件渲染的层级

把条件渲染逻辑上移到Portal外层,让AnimatePresence直接包裹需要挂载/卸载的Portal内容,保证动画触发逻辑正常:
首先修改index.js:

// index.js 修改后
import React from "react";
import ReactDOM from "react-dom";
import { AnimatePresence } from "framer-motion";
import Modal from "./Modal";
import { ContextProvider, useContext } from "./Context";
import "./styles.css";

const REACT_ROOT = document.getElementById("react-root");
const PORTAL_ROOT = document.getElementById("portal");

const PortalModal = () => {
  const { activeItem } = useContext();
  if (!PORTAL_ROOT) return null;
  // 仅当activeItem不是-1时渲染Modal,让AnimatePresence感知整个Modal的挂载卸载
  return ReactDOM.createPortal(
    <AnimatePresence exitBeforeEnter>
      {activeItem !== -1 && <Modal key="modal" />}
    </AnimatePresence>, 
    PORTAL_ROOT
  );
};

const App = () => (
  <ContextProvider>
    <PortalModal />
  </ContextProvider>
);

ReactDOM.render(<App />, REACT_ROOT);

然后修改Modal.js,移除内部的AnimatePresence,保留motion元素的动画配置:

// Modal.js 修改后
import React from "react";
import { m } from "framer-motion";
import { useContext } from "./Context";

const Modal = () => {
  const { activeItem, setActiveItem } = useContext();

  return (
    <>
      {activeItem === 0 && (
        <m.div
          key={"step-1"}
          className={"step"}
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
          exit={{ opacity: 0 }}
        >
          <h2>STEP 1</h2>
          <button onClick={() => setActiveItem(-1)}>CLOSE</button>
          <button onClick={() => setActiveItem(1)}>NEXT</button>
        </m.div>
      )}
      {activeItem === 1 && (
        <m.div
          key={"step-2"}
          className={"step"}
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
          exit={{ opacity: 0 }}
        >
          <h2>STEP 2</h2>
          <button onClick={() => setActiveItem(-1)}>CLOSE</button>
          <button onClick={() => setActiveItem(0)}>PREV</button>
        </m.div>
      )}
    </>
  );
};

export default Modal;

3. 可选排查项

如果修复后还是有问题,检查你的.step类对应的CSS样式,不要手动设置opacity或者transition属性,避免和Framer Motion生成的内联样式冲突。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.23 23:15:04