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

TypeScript项目中AnimatePresence关闭动画失效问题排查求助

解决Framer Motion AnimatePresence关闭模态框动画失效问题

问题诊断

你的代码存在三个核心问题导致关闭动画无法触发:

  1. AnimatePresence未包裹条件渲染元素:组件开头的if (!isModalOpen) return null直接卸载了整个组件(包括AnimatePresence),Framer Motion没有机会执行exit动画。
  2. 状态管理与关闭逻辑冲突:关闭时同步调用setIsModalOpen(false)和onClose(),导致外部状态立即更新,组件提前被卸载,动画还没执行就结束了。
  3. 缺少唯一key标识:AnimatePresence下的motion元素需要唯一key来追踪元素的进入/退出状态,否则无法正确识别动画触发时机。

修复方案

以下是修改后的完整代码,关键改动已标注:

import {
  FC,
  PropsWithChildren,
  useEffect,
  useMemo,
  useRef,
  useState,
} from 'react';
import ReactDOM from 'react-dom';
import ModalBody from './ModalBody';
import ModalContext, { ModalContextValue } from './ModalContext';
import ModalFooter from './ModalFooter';
import ModalHeader from './ModalHeader';
import { motion, AnimatePresence, MotionProps } from 'framer-motion';

const dropIn = {
  hidden: {
    y: '-100vh',
    opacity: 0,
  },
  visible: {
    y: '0',
    opacity: 1,
    transition: {
      duration: 1,
      type: 'spring',
      damping: 15,
      stiffness: 200,
    },
  },
  exit: {
    y: '100vh',
    opacity: 0,
  },
};

export interface ModalProps extends PropsWithChildren {
  isOpen: boolean;
  onClose: () => void;
  onOpen?: () => void;
  width?: string;
}

const Modal: FC<ModalProps & MotionProps> = (props) => {
  const { isOpen, onClose, onOpen, children, width } = props;
  const [isModalOpen, setIsModalOpen] = useState<boolean>(isOpen);
  const modalRef = useRef<HTMLDivElement>(null);

  const contextValue = useMemo<ModalContextValue>(() => {
    return {
      handleClose: () => setIsModalOpen(false), // 仅更新内部状态触发动画
    };
  }, []);

  // 同步外部isOpen状态到内部
  useEffect(() => {
    if (isOpen) {
      setIsModalOpen(true);
    }
  }, [isOpen]);

  useEffect(() => {
    if (isModalOpen) {
      document.body.style.overflow = 'hidden';
      modalRef.current?.focus();
      onOpen?.();
    } else {
      document.body.style.overflow = 'auto';
    }
  }, [isModalOpen]);

  const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
    if (event.key === 'Escape') {
      setIsModalOpen(false);
    }
  };

  // 移除直接return null的逻辑,让AnimatePresence始终存在
  return ReactDOM.createPortal(
    <ModalContext.Provider value={contextValue}>
      <AnimatePresence
        // 动画完成后再通知外部关闭
        onExitComplete={() => {
          onClose();
        }}
      >
        {/* 在这里做条件渲染,AnimatePresence会追踪这个元素的进入/退出 */}
        {isModalOpen && (
          <motion.div
            key="modal-backdrop" // 添加唯一key
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            exit={{ opacity: 0 }}
            transition={{ duration: 1 }}
            ref={modalRef}
            className="fixed inset-0 z-50 overflow-y-auto flex items-center justify-center min-h-screen p-6"
            tabIndex={-1}
            role="dialog"
            aria-modal="true"
            aria-labelledby="modal-title"
            onKeyDown={handleKeyDown}
          >
            <div
              className="fixed inset-0"
              aria-hidden="true"
              onClick={() => setIsModalOpen(false)}
            >
              <div className="absolute inset-0 bg-gray-500 opacity-75"></div>
            </div>

            <motion.div
              variants={dropIn}
              initial="hidden" // 使用variants的key代替直接传对象
              animate="visible"
              exit="exit"
              className={`
            bg-iit-gray text-white rounded-md text-left overflow-hidden
            shadow-xl transform  sm:max-w-lg sm:w-full
            md:max-w-3xl h-full max-h-[90vh] ${width}`}
              role="document"
            >
              {children}
            </motion.div>
          </motion.div>
        )}
      </AnimatePresence>
    </ModalContext.Provider>,
    document.body
  );
};

Modal.displayName = 'Modal';

export default Object.assign(Modal, {
  Header: ModalHeader,
  Body: ModalBody,
  Footer: ModalFooter,
});

关键改动说明

  • AnimatePresence始终渲染:移除组件开头的条件return,让AnimatePresence一直存在于DOM中,确保能追踪子元素的退出动画。
  • 分离内部动画状态与外部状态:关闭时仅更新内部isModalOpen触发动画,动画完成后通过onExitComplete调用外部onClose,避免组件提前卸载。
  • 添加唯一key:给AnimatePresence内的motion根元素添加key="modal-backdrop",让Framer Motion能正确识别元素的进入/退出。
  • 简化variants调用:使用initial="hidden"代替直接传入dropIn.hidden,符合Framer Motion的最佳实践。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.16 06:52:31