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

如何单独控制Headless UI Disclosure的展开与收起状态

问题解决:Headless UI Disclosure 组件同步展开问题

问题描述

使用Headless UI的Disclosure组件时,打开一个折叠面板,其他所有面板会同步展开(虽内容不显示但存在空白占位),需要实现单独控制每个面板的展开/收起状态。

问题原因

出现这种同步展开的情况,通常是因为:

  • 多个Disclosure实例的状态被意外共享
  • 循环渲染CardValue组件时未指定唯一key,导致React复用组件实例,引发状态混乱

修复方案

提供两种可靠解决方式,可根据需求选择:

方式一:循环渲染时添加唯一key

如果通过数组循环渲染多个CardValue,必须给每个组件添加唯一key属性,让React能区分不同组件实例,避免状态复用。

示例代码:

{cardList.map((card) => (
  <CardValue
    key={card.id} // 使用数据的唯一ID作为key
    icon={<ChartBarIcon className='text-white h-5 w-5' />}
    title={card.title}
    subtitle={card.subtitle}
    // 其他属性...
  >
    {/* 子内容 */}
  </CardValue>
))}

方式二:手动管理Disclosure状态(灵活可控)

通过父组件传递状态和回调,让每个CardValue的展开状态独立可控,彻底避免状态共享问题。

修改后的CardValue组件

import React from 'react';
import { Disclosure } from '@headlessui/react';
import { ChevronUpIcon } from '@heroicons/react/solid';
import { Ratio } from '../ratio/Ratio';

export const CardValue = ({
  icon,
  title,
  subtitle,
  children,
  subtitleClassName,
  selected,
  arrow,
  arrowValue,
  // 新增手动控制的状态与回调
  isOpen,
  onToggle,
}) => {
  return (
    <div className='w-full max-w-[325px] rounded-lg bg-white p-2'>
      {/* 传入手动状态与回调 */}
      <Disclosure open={isOpen} onOpenChange={onToggle}>
        {({ open }) => (
          <>
            <Disclosure.Button className='flex w-full justify-between rounded-lg px-4 py-2 text-left text-sm font-medium focus:outline-none'>
              <div className='bg-[#01B5A3] rounded-lg p-2'>{icon}</div>
              <div className='flex flex-col space-y-3'>
                <label className='text-lg'>{title}</label>
                <label
                  className={`text-2xl font-semibold ${subtitleClassName}`}>
                  {subtitle}
                </label>
              </div>
              <div className='flex flex-col space-y-5 items-center'>
                <ChevronUpIcon
                  className={`${
                    open ? 'rotate-180 transform' : ''
                  } h-5 w-5 text-[#727272]`}
                />
              </div>
            </Disclosure.Button>
            <Disclosure.Panel className='px-4 pt-4 pb-2 text-sm text-gray-500'>
              {children}
            </Disclosure.Panel>
          </>
        )}
      </Disclosure>
    </div>
  );
};

父组件中使用示例

import React, { useState } from 'react';
import { CardValue } from './CardValue';
import { ChartBarIcon } from '@heroicons/react/solid';

// 示例数据列表
const cardData = [
  { id: 1, title: 'Sales Converted', total: 1200 },
  { id: 2, title: 'Leads Generated', total: 340 },
  // 更多数据...
];

const ParentComponent = () => {
  // 用对象存储每个卡片的展开状态
  const [openStates, setOpenStates] = useState({});

  const handleToggle = (cardId) => {
    setOpenStates(prev => ({
      ...prev,
      [cardId]: !prev[cardId]
    }));
  };

  return (
    <div className='flex flex-wrap gap-4'>
      {cardData.map((card) => (
        <CardValue
          key={card.id}
          icon={<ChartBarIcon className='text-white h-5 w-5' />}
          title={card.title}
          subtitle={`${detailStore?.currency} ${
            card.total == 0 ? 'N/A' : card.total
          }`}
          subtitleClassName={`${
            card.total == 0 ? 'text-na_text' : 'text-black'
          }`}
          isOpen={openStates[card.id] || false}
          onToggle={() => handleToggle(card.id)}
        >
          {/* 子内容 */}
          <hr />
          <div className='flex flex-1 flex-col mt-6 mb-4 space-y-4 text-sm'>
            {/* 子内容项 */}
          </div>
        </CardValue>
      ))}
    </div>
  );
};

export default ParentComponent;

方案说明

  • 方式一适合简单场景,仅需保证循环渲染时key唯一即可
  • 方式二更适合需要精准控制面板状态的场景,比如默认展开指定面板、联动控制多个面板等

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.29 05:12:15