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

NextJS中React Portal侧边栏点击关闭按钮动画失效求助

NextJS中React Portal滑出菜单关闭动画失效问题

我在NextJS项目中用React Portal实现了从屏幕右侧滑出的全屏菜单,采用CSS @keyframes实现显隐动画,菜单显示动画正常,但关闭时存在异常:

  • 通过Esc键、菜单选项关闭时动画正常
  • 点击原打开按钮关闭时动画失效

考虑到Portal在状态变为false时会直接删除内容,我选择为内部容器添加动画,通过点击事件触发动画后用setTimeout延迟调用关闭方法,但推测可能是组件间两个useState状态分配不当导致问题。以下是Header、SlideScreen、Nav三个组件的完整代码,请求排查问题。

// header.js
import React, { useState, useEffect } from "react";
import Link from "next/link";
import ThemeToggle from "@/components/themeSwitcher";
import SlideScreen from "@/components/global/slideScreen";
import styles from "@/styles/header.module.css"

export default function Header() {

    const headerInnerClasses = [styles.headerInner, "wrapper"].join(" ")

    const [showSlideScreen, setShowSlideScreen] = useState(false);

    const handleClick = () => {
        setShowSlideScreen(!showSlideScreen);
    };

    const menuBtnClasses = [showSlideScreen ? `${styles.menuBtnActive} ${styles.menuBtn}` : `${styles.menuBtn}`, "mainBtn"].join(" ")

    useEffect(() => {
        document.body.className = showSlideScreen ? "isSlideActive" : "";
    });

    return (
        <header className={styles.header}>
            <div className={headerInnerClasses}>
                <Link className={styles.headerLogo} href='/'>
                    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 160"><path d="M10 20 0 29.9v-5.5L5 20zM28 20 0 46.9v-5.5L22 20zM40 27 0 64.9v-5.5L40 21zM40 44 0 81.9v-5.5L40 38zM40 61 0 98.9v-5.5L40 55zM40 78 0 115.9v-5.5L40 72zM40 95 0 132.9v-5.5L40 89zM40 112 0 149.9v-5.5L40 106zM40 129 7 160H1l39-37zM40 146l-15 14h-6l21-20zM40 160h-3l3-3z" fill="#494949"/><path d="M128 107.7 69.7 73.9l58.3-34V0L0 73.9 88 126v34h40z" fill="#f65f59"/></svg>
                </Link>

                <div className={styles.headerActions}>
                    <ThemeToggle/>
                    <button className={menuBtnClasses} onClick={handleClick}>
                        <div className={styles.burger}></div>
                    </button>
                </div>
            </div>
            {showSlideScreen &&
                <SlideScreen
                    onClose={() => setShowSlideScreen(false)}
                    showSlideScreen={showSlideScreen}
                />}
        </header>
    )
}
// slideScreen.js
import React, { useEffect, useState, useRef } from "react";
import ReactDOM from "react-dom";
import styles from "@/styles/slideScreen.module.css"
import Nav from "@/components/nav";

//import { CSSTransition } from "react-transition-group";

const SlideScreen = ({onClose}) => {

    const [isBrowser, setIsBrowser] = useState(false);

    const [endAnimation, setEndAnimation] = useState(false);

    const checkIfClickedEsc = e => {
        if ( e.code === "Escape" ) {
            setEndAnimation(true);
            setTimeout(() => {
                onClose()
            }, 100)
        }
    }

    const handleCloseClick = () => {
        setEndAnimation(true);
        setTimeout(() => {
            onClose();
        }, 100)
    };

    useEffect(() => {
        setIsBrowser(true);
        document.addEventListener("keydown", checkIfClickedEsc)
        return () => {
            document.removeEventListener("keydown", checkIfClickedEsc)
        }

    }, []);

    const inScreenClasses = [endAnimation ?  `${styles.inScreen} ${styles.close}`  : `${styles.inScreen}`, "dotsBg"].join(" ")

    const slideScreenContent = (
        <div className={styles.slideScreen}>
            <div className={inScreenClasses}>
                <div className="wrapper">
                    <Nav handleCloseClick={handleCloseClick} />
                </div>
            </div>
        </div>
    )

    if (isBrowser) {
        return ReactDOM.createPortal(
            slideScreenContent,
            document.getElementById("modal-root")
        )
    } else {
        return null;
    }

}

export default SlideScreen;
// nav.js
import React, { useState } from "react";
import Link from "next/link";
import {useRouter} from "next/router";

import styles from "@/styles/header.module.css"

const Nav = ({handleCloseClick}) => {
    const router = useRouter();
    const routes = ['Blog', 'Work', 'Contact'];
    const routesRu = ['Блог', 'Работы', 'Контакты'];

    return (
        <nav className={styles.headerNav}>
            <ul className={styles.headerMenu}>
                {routes.map((route, i) => {
                    return (
                        <li key={route} className={`${router.pathname === `/${route.toLowerCase()}`}`}>
                            <Link
                                href={`/${route.toLowerCase()}`}
                                onClick={handleCloseClick}
                                >{routesRu[i]}</Link>
                        </li>
                    )
                })}
            </ul>
        </nav>
    )

}

export default Nav

内容的提问来源于stack exchange,提问作者Лео

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.16 13:07:41