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

React状态变更时CSS Transition失效问题求助

React手风琴关闭时无过渡效果的问题排查与修复

问题描述

实现了React手风琴组件,点击时预期内容通过过渡效果显示/隐藏,但仅展开时有过渡,关闭时无平滑过渡。

代码片段

JSX代码

import styles from './ChapterList.module.scss';
import {useEffect, useState} from "react";
import {AccordionCloseIcon, AccordionShowIcon} from "../../../components/icons/Accordion";

const dummyChapterContents = [{
    id: 'c1',
    type: 'লেকচার ক্লাস ০১',
    title: 'পৌণঃপুনিক থেকে সাধারণ ভগ্নাংশে রূপান্তর থেকে',
    data: '1 Feb 2023 '
}, {id: 'c2', title: 'Lorem ipsum'}, {id: 'c3', title: 'Lorem ipsum'}]

const ChapterList = ({chapters, onSelectChapterContent}) => {
    const [activeChapterId, setActiveChapterId] = useState(null);
    const [activeChapterContentId, setActiveChapterContentId] = useState(null);
    const [activeChapterContents, setActiveChapterContents] = useState([]);

    const handleChapterClick = (id) => {
        setActiveChapterId(activeChapterId === id ? null : id);
    };

    const handleChapterContentClick = (info) => {
        setActiveChapterContentId(info?.id);
        onSelectChapterContent(info)
    };

    // chapter contents related logic go here
    const renderContent = () => {
        // API logics [loading, fill/empty, error] to be written
        if (!activeChapterContents.length) {
            return null;
        }

        return (
            <ul>
                {activeChapterContents.map((item) => (
                    <li
                        key={`${activeChapterId}-${item?.id}`}
                        className={`${styles.chapter_content__item} ${
                            activeChapterContentId === item?.id && styles.chapter_content__item__active
                        }`}
                        onClick={() => handleChapterContentClick(item)}
                    >
                        {item?.title}
                    </li>
                ))}
            </ul>
        );
    }

    useEffect(() => {
        if (activeChapterId) {
            // fetch chapter contents from API
            setActiveChapterContents(dummyChapterContents)
        }
    }, [activeChapterId])

    return (
        <div className={`col-span-full md:col-span-5 ${styles.chapter_list}`}>
            {chapters.map((chapter) => (
                <div
                    key={chapter.id}
                    className={`${styles.chapter_item} ${activeChapterId === chapter.id ? styles.active : ''}`}
                >
                    <div className={styles.chapter_meta_info} onClick={() => handleChapterClick(chapter.id)}>
                        <div>
                            <div className={styles.chapter_title}>{chapter.title}</div>
                        </div>
                        <div className={styles.chapter_icon}>
                            {
                                activeChapterId === chapter.id ? <AccordionShowIcon/> : <AccordionCloseIcon/>
                            }
                        </div>
                    </div>
                    <div className={styles.chapter_content}>
                        {activeChapterId === chapter?.id && renderContent()}
                    </div>
                </div>
            ))}
        </div>
    );
};

export default ChapterList;

SCSS代码

.chapter_list {
  width: 100%;
  display: flex;
  flex-direction: column;
  row-gap: 12px;

  @media (min-width: 768px) {
    row-gap: 16px;
  }
}

.chapter_item {
  background-color: #FFFFFF;
  border: 1px solid #D0DEEF;
  border-radius: 8px;
  padding: 16px;

  @media (min-width: 768px) {
    border-radius: 10px;
    padding: 20px 12px;
  }

  .chapter_meta_info {
    cursor: pointer;
    display: flex;
    justify-content: space-between;
    align-items: center;

    .chapter_title {
      font-family: 'Baloo Da 2';
      font-style: normal;
      font-weight: 500;
      font-size: 16px;
      line-height: 27px;
      color: #343B6D;

      @media (min-width: 768px) {
        font-weight: 600;
      }
    }

    .chapter_icon {
      font-size: 1.2rem;
      transition: transform 0.5s ease-in-out;
    }
  }

  .chapter_content {
    max-height: 0;
    overflow: auto;
    transition: max-height 0.5s ease-in-out;
    padding: 0 10px;

    &__item {
      list-style: none;
      margin: 0;
      padding: 0.5rem 0;
      cursor: pointer;
      font-size: 1rem;
      font-weight: bold;
      color: #333;
      border-bottom: 1px solid #ddd;
      transition: all 0.5s ease-in-out;

      &:hover {
        background-color: #f6f6f6;
      }

      &__active {
        background-color: #f6f6f6;
        color: #0070f3;
      }

      @media (min-width: 768px) {
        font-size: 1.2rem;
        padding: 1rem 0;
      }
    }
  }

  &.active {
    .chapter_meta_info {
      .chapter_title {
      }

      .chapter_icon {
        transform: rotate(360deg);
      }
    }

    .chapter_content {
      max-height: 600px;
    }
  }
}

问题原因

关闭手风琴时,activeChapterId !== chapter.id会触发条件渲染,直接移除.chapter_content内的内容节点。此时.chapter_content的内容为空,高度瞬间变为0,CSS的max-height过渡没有可计算的高度变化过程,导致过渡失效。

修复方案

1. 修改JSX的条件渲染逻辑

将.chapter_content内的条件渲染移除,始终保留内容节点,依赖CSS的max-height控制显示隐藏:

// 原代码
<div className={styles.chapter_content}>
    {activeChapterId === chapter?.id && renderContent()}
</div>

// 修改后
<div className={styles.chapter_content}>
    {renderContent()}
</div>

2. 优化状态逻辑(可选)

如果需要在关闭手风琴时清空内容,可以调整useEffect,确保过渡完成后再清空:

useEffect(() => {
    if (activeChapterId) {
        setActiveChapterContents(dummyChapterContents);
    } else {
        // 延迟清空,等待过渡完成
        const timer = setTimeout(() => {
            setActiveChapterContents([]);
        }, 500); // 与transition时长一致
        return () => clearTimeout(timer);
    }
}, [activeChapterId]);

3. 调整SCSS(可选)

如果关闭时滚动条闪烁影响体验,可以给.chapter_content添加过渡时的overflow控制:

.chapter_content {
    max-height: 0;
    overflow: hidden; /* 默认隐藏滚动条 */
    transition: max-height 0.5s ease-in-out;
    padding: 0 10px;

    /* 展开时显示滚动条 */
    .chapter_item.active & {
        overflow: auto;
    }

    // ... 其他样式
}

原理说明

修改后,关闭手风琴时,内容节点不会被立即移除,.chapter_content会从max-height:600px平滑过渡到max-height:0,同时overflow:auto/hidden会裁剪内容,实现关闭时的平滑过渡效果。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.23 04:10:06