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

React Spring动画下拉列表:折叠时内容无法保留状态问题

解决React Spring Transition列表展开/折叠动画异常问题

嘿,这个问题我之前也碰到过!核心问题就是你所有列表项共用一个状态属性,导致React Spring的Transition组件没法追踪每个项独立的进入/退出状态变化,自然就出不来正确的动画效果了。咱们一步步来解决:

问题根源拆解

  • 当你用单个listContent状态存储所有项的内容时,点击新项会直接覆盖旧值,Transition组件还没来得及处理旧内容的退出动画,就被新内容替换了
  • 折叠当前项时,状态直接清空,Transition没有检测到"退出"的状态过渡过程,所以内容会直接消失

解决方案:给每个列表项独立维护状态

最靠谱的做法是用对象或Map存储每个项的展开状态与内容,让每个列表项的状态完全独立,互不干扰。

示例代码实现

import { useState } from 'react';
import { Transition } from 'react-spring';

const ExpandableList = ({ items }) => {
  // 用对象存储每个项的展开状态和内容:{ 项ID: { isOpen: 布尔值, content: 内容数据 } }
  const [expandedItems, setExpandedItems] = useState({});

  const handleItemClick = async (itemId) => {
    const currentItemState = expandedItems[itemId] || { isOpen: false, content: null };

    if (currentItemState.isOpen) {
      // 折叠操作:先标记为关闭,让Transition执行退出动画,内容可以保留(下次展开直接复用)
      setExpandedItems(prev => ({
        ...prev,
        [itemId]: { ...prev[itemId], isOpen: false }
      }));
    } else {
      // 展开操作:先触发进入动画,再异步加载内容
      setExpandedItems(prev => ({
        ...prev,
        [itemId]: { ...prev[itemId], isOpen: true }
      }));
      
      // 模拟从数据库获取内容的异步操作
      const fetchedContent = await fetchItemContent(itemId);
      setExpandedItems(prev => ({
        ...prev,
        [itemId]: { ...prev[itemId], content: fetchedContent }
      }));
    }
  };

  return (
    <ul style={{ listStyle: 'none', padding: 0 }}>
      {items.map(item => {
        const { isOpen, content } = expandedItems[item.id] || { isOpen: false, content: null };
        return (
          <li 
            key={item.id} 
            onClick={() => handleItemClick(item.id)}
            style={{ padding: '10px', borderBottom: '1px solid #eee', cursor: 'pointer' }}
          >
            {item.title}
            {/* 每个项绑定独立的Transition组件 */}
            <Transition
              items={isOpen}
              from={{ height: 0, opacity: 0, overflow: 'hidden' }}
              enter={{ height: 'auto', opacity: 1 }}
              leave={{ height: 0, opacity: 0 }}
              config={{ duration: 300 }}
            >
              {/* 根据isOpen状态决定是否渲染内容容器 */}
              {shouldRender => shouldRender && (props => (
                <div style={{ ...props, marginTop: '10px', padding: '10px', backgroundColor: '#f5f5f5' }}>
                  {content || '加载中...'}
                </div>
              ))}
            </Transition>
          </li>
        );
      })}
    </ul>
  );
};

// 模拟从数据库获取内容的函数
const fetchItemContent = async (itemId) => {
  await new Promise(resolve => setTimeout(resolve, 500));
  return `这是项 ${itemId} 的详细内容,从数据库加载完成啦!`;
};

关键优化点

  1. 独立状态管理:每个列表项的展开状态和内容都存在expandedItems对象中,用项ID作为键,确保状态互不干扰
  2. 动画与加载分离:展开时先触发进入动画,再异步加载内容,避免等待数据导致动画延迟
  3. 明确的状态过渡:折叠时只修改isOpen状态,让Transition组件能检测到"退出"信号,执行完整的折叠动画

如果不想保留已加载的内容,也可以在退出动画完成后再清空对应项的内容,React Spring的Transition支持onRest回调,可以用来做这件事:

<Transition
  // ...其他配置
  onRest={() => {
    if (!isOpen) {
      setExpandedItems(prev => {
        const newState = {...prev};
        delete newState[itemId]; // 动画完成后清空状态
        return newState;
      });
    }
  }}
>
  {/* ...内容渲染 */}
</Transition>

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.14 08:35:04