TypeScript项目中AnimatePresence关闭动画失效问题排查求助
解决Framer Motion AnimatePresence关闭模态框动画失效问题
问题诊断
你的代码存在三个核心问题导致关闭动画无法触发:
- AnimatePresence未包裹条件渲染元素:组件开头的
if (!isModalOpen) return null直接卸载了整个组件(包括AnimatePresence),Framer Motion没有机会执行exit动画。 - 状态管理与关闭逻辑冲突:关闭时同步调用
setIsModalOpen(false)和onClose(),导致外部状态立即更新,组件提前被卸载,动画还没执行就结束了。 - 缺少唯一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
相关产品推荐
相关产品推荐

