React useState钩子下CSS关闭动画失效问题及修复咨询
滑动子菜单关闭动画失效问题排查与修复
问题描述
我用React、TypeScript和Tailwind构建了一个滑动子菜单,通过useState控制子菜单显示状态。打开动画data-[state=open]:animate-[submenu-show_300ms]正常工作,但关闭动画data-[state=closed]:animate-[submenu-hide_300ms]完全没效果,尝试了两种实现方式都没能解决:
最初实现(条件渲染卸载组件)
{clickedItem === item && ( <ul data-state={dataSate} className="fixed top-0 flex h-svh w-[300px] flex-col bg-red-300 ease-in data-[state=closed]:animate-[submenu-hide_300ms] data-[state=open]:animate-[submenu-show_300ms]" > <li> <button onClick={() => setClickedItem(null)} type="button" className="flex items-center p-2" > <span className="mr-10">Back</span> <span className="font-bold">{item.title}</span> </button> </li> {item.items?.map((item) => ( <li key={item.url}> {item.items && item.items.length > 0 ? ( <button onClick={() => handleClick(item)} className="flex w-full justify-between p-2" type="button" > {item.title} <span>[+]</span> </button> ) : ( <a href={item.url} className="p-2"> {item.title} </a> )} </li> ))} </ul> )}
改进尝试(CSS控制显示隐藏)
<ul data-state={dataSate} className={`${clickedItem === item ? 'left-0 animate-[submenu-show_300ms]' : 'left-[-375px] animate-[submenu-hide_300ms] overflow-hidden'} fixed top-0 flex h-svh w-[375px] flex-col bg-red-300 ease-in`} > <li> <button onClick={() => setClickedItem(null)} type="button" className="flex items-center p-2" > <span className="mr-10">Back</span> <span className="font-bold">{item.title}</span> </button> </li> {item.items?.map((item) => ( <li key={item.url}> {item.items && item.items.length > 0 ? ( <button onClick={() => handleClick(item)} className="flex w-full justify-between p-2" type="button" > {item.title} <span>[+]</span> </button> ) : ( <a href={item.url} className="p-2"> {item.title} </a> )} </li> ))} </ul>
问题根源
- 第一种实现:关闭时直接通过
clickedItem === item的条件将组件从DOM中卸载,动画还未执行完成就被移除,自然看不到关闭效果。 - 第二种实现:切换
left属性的同时绑定自定义动画类,Tailwind的animate-*关键帧动画与直接修改left的过渡逻辑冲突,且类名切换时没有预留动画执行的时间窗口。
修复方案
方案一:用过渡类替代自定义动画(推荐)
放弃自定义关键帧动画,改用Tailwind内置的过渡类实现滑动效果,同时确保组件在动画完成后再卸载:
import { useState } from 'react'; // 假设MenuItem类型定义 type MenuItem = { title: string; url?: string; items?: MenuItem[]; }; export default function SlideMenu() { const [activeItem, setActiveItem] = useState<MenuItem | null>(null); const [isMenuVisible, setIsMenuVisible] = useState(false); const openSubmenu = (item: MenuItem) => { setActiveItem(item); setIsMenuVisible(true); }; const closeSubmenu = () => { setIsMenuVisible(false); // 动画结束后卸载组件 setTimeout(() => setActiveItem(null), 300); }; // 示例菜单数据 const menuItems: MenuItem[] = [ { title: "菜单1", items: [ { title: "子项1-1", url: "/sub1-1" }, { title: "子项1-2", url: "/sub1-2" } ] }, { title: "菜单2", items: [ { title: "子项2-1", url: "/sub2-1" }, { title: "子项2-2", items: [{ title: "嵌套子项", url: "/nested" }] } ] } ]; return ( <div className="flex"> {/* 主菜单 */} <ul className="w-[300px] bg-gray-100"> {menuItems.map((item) => ( <li key={item.title}> <button onClick={() => openSubmenu(item)} className="flex w-full justify-between p-2" > {item.title} <span>[+]</span> </button> </li> ))} </ul> {/* 滑动子菜单 */} {activeItem && ( <ul className={`fixed top-0 left-0 flex h-svh w-[300px] flex-col bg-red-300 transition-all duration-300 ease-in ${isMenuVisible ? 'translate-x-0' : '-translate-x-full'}`} > <li> <button onClick={closeSubmenu} className="flex items-center p-2"> <span className="mr-10">Back</span> <span className="font-bold">{activeItem.title}</span> </button> </li> {activeItem.items?.map((subItem) => ( <li key={subItem.url || subItem.title}> {subItem.items?.length ? ( <button onClick={() => openSubmenu(subItem)} className="flex w-full justify-between p-2" > {subItem.title} <span>[+]</span> </button> ) : ( <a href={subItem.url} className="block p-2"> {subItem.title} </a> )} </li> ))} </ul> )} </div> ); }
方案二:修复自定义关键帧动画
如果坚持使用自定义动画,需要确保关键帧定义正确,且动画完成后再卸载组件:
- 配置Tailwind自定义关键帧:
// tailwind.config.js module.exports = { content: ["./src/**/*.{js,jsx,ts,tsx}"], theme: { extend: { animation: { 'submenu-show': 'submenu-show 300ms ease-in forwards', 'submenu-hide': 'submenu-hide 300ms ease-in forwards', }, keyframes: { 'submenu-show': { '0%': { transform: 'translateX(-100%)' }, '100%': { transform: 'translateX(0)' }, }, 'submenu-hide': { '0%': { transform: 'translateX(0)' }, '100%': { transform: 'translateX(-100%)' }, }, }, }, }, plugins: [], }
- 修改子菜单JSX:
{activeItem && ( <ul data-state={isMenuVisible ? 'open' : 'closed'} className="fixed top-0 flex h-svh w-[300px] flex-col bg-red-300 data-[state=open]:animate-submenu-show data-[state=closed]:animate-submenu-hide" onAnimationEnd={() => { if (!isMenuVisible) setActiveItem(null); }} > <li> <button onClick={closeSubmenu} className="flex items-center p-2"> <span className="mr-10">Back</span> <span className="font-bold">{activeItem.title}</span> </button> </li> {/* 子项内容 */} </ul> )}
核心修复要点
- 永远不要在动画执行时直接卸载组件,必须等动画完成后再处理DOM移除。
- 用状态控制动画触发时机,关闭时先切换动画状态,再延迟更新卸载状态。
- 自定义关键帧动画要确保
forwards属性保留动画最终状态,避免元素跳回初始位置。
内容的提问来源于stack exchange,提问作者meez
相关产品推荐
相关产品推荐

