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

React嵌套折叠面板(Accordion)父组件高度不更新问题求助

React嵌套折叠面板(Accordion)父组件高度不更新问题求助

嘿,我完全懂你遇到的这个痛点!嵌套Accordion最容易踩这个坑——父折叠面板在子面板展开后,没法自动感知内容高度变化,导致第一次打开子项目时,父容器的高度还是初始的小尺寸,内容显示不全对吧?我来给你两个实用的解决方案,结合你的现有代码来拆解:

问题根源分析

你的父Accordion的高度是在自身初始化或者isActive状态变化时计算的,但子Accordion展开会动态改变父内容区域的scrollHeight,而父组件没有监听这个动态变化,所以第一次打开子项目时,父的高度不会自动更新。


方案一:用ResizeObserver自动监听内容高度变化(推荐)

这是最简洁的方案,不需要修改组件结构,只需要在你的Accordion.tsx里添加一个ResizeObserver,实时监听内容区域的尺寸变化,当内容高度改变时自动更新父容器的高度:

修改Accordion.tsx的代码:

"use client"
import { Component } from "@/shared/types"
import React, { MutableRefObject, ReactNode, RefObject, useEffect, useRef, useState } from "react"
import { Heading } from "@/components/atoms/Heading"

export interface AccordionProps extends Component {
  title: string
  children: ReactNode
  isActive?: boolean
  onClick?: () => void
  headingSize?: "h1" | "h2"
}

export function Accordion({
  className = "",
  title,
  children,
  isActive,
  onClick,
  headingSize = "h1",
  testId = "Accordion",
}: AccordionProps) {
  const [isOpen, setIsOpen] = useState(false)
  const [height, setHeight] = useState("0px")
  const contentHeight = useRef(null) as MutableRefObject<HTMLElement | null>
  // 新增:用于监听内容尺寸变化的Observer引用
  const resizeObserverRef = useRef<ResizeObserver | null>(null)

  // 处理isActive外部控制的逻辑
  useEffect(() => {
    if (isActive === undefined || !contentHeight.current) return
    setHeight(isActive ? `${contentHeight.current.scrollHeight}px` : "0px")
    setIsOpen(isActive)
  }, [isActive])

  // 新增:监听内容区域的尺寸变化
  useEffect(() => {
    if (!contentHeight.current) return

    // 创建ResizeObserver,当内容高度变化时更新Accordion高度
    resizeObserverRef.current = new ResizeObserver((entries) => {
      for (const entry of entries) {
        // 只有当Accordion处于打开状态时才更新高度
        if (isOpen || (isActive !== undefined && isActive)) {
          setHeight(`${entry.target.scrollHeight}px`)
        }
      }
    })

    resizeObserverRef.current.observe(contentHeight.current)

    // 组件卸载时断开监听
    return () => {
      resizeObserverRef.current?.disconnect()
    }
  }, [isOpen, isActive])

  function handleToggle() {
    if (!contentHeight?.current) return
    const newIsOpen = !isOpen
    setIsOpen(newIsOpen)
    setHeight(newIsOpen ? `${contentHeight.current.scrollHeight}px` : "0px")
    onClick?.()
  }

  return (
    <div className={`w-full text-lg font-medium text-left focus:outline-none ${className}`}>
      <button onClick={handleToggle} data-testid={testId}>
        <Heading
          className="flex items-center justify-between"
          color={isActive ? "text-blue-200" : "text-white"}
          level={headingSize}
        >
          {title}
        </Heading>
      </button>
      <div
        className={`overflow-hidden transition-max-height duration-250 ease-in-out`}
        ref={contentHeight as RefObject<HTMLDivElement>}
        style={{ maxHeight: height }}
      >
        <div className="pt-2 pb-4">{children}</div>
      </div>
    </div>
  )
}

为什么这个方案有效?

ResizeObserver会实时监测contentHeight容器的尺寸变化——当你打开子Accordion时,子内容的高度会增加,父容器的scrollHeight也会跟着变,这时Observer就会触发,自动更新父Accordion的maxHeight,完美解决第一次打开时高度不对的问题。


方案二:通过回调函数让子组件通知父组件更新高度

如果你不想用ResizeObserver,也可以通过自定义回调的方式,让子Accordion在状态变化时通知父组件重新计算高度:

  1. 首先修改AccordionProps,添加一个回调属性:
export interface AccordionProps extends Component {
  // 原有属性...
  onContentHeightChange?: () => void; // 新增回调
}
  1. 在Accordion的handleToggle函数中,触发这个回调:
function handleToggle() {
  if (!contentHeight?.current) return
  const newIsOpen = !isOpen
  setIsOpen(newIsOpen)
  setHeight(newIsOpen ? `${contentHeight.current.scrollHeight}px` : "0px")
  onClick?.()
  // 通知父组件内容高度变化
  onContentHeightChange?.()
}
  1. 在父Accordion(也就是Homepage里的Portfolio所在的Accordion)中,添加ref并处理回调:
// 在Homepage.tsx中引入useRef
import { useRef } from "react"

export function Homepage({ className = "", testId = "homepage" }: HomepageProps) {
  const portfolioAccordionRef = useRef<{ updateHeight: () => void }>(null)

  // 定义更新父Accordion高度的方法
  const updatePortfolioAccordionHeight = () => {
    if (!portfolioAccordionRef.current) return
    portfolioAccordionRef.current.updateHeight()
  }

  return (
    <main className={`grid grid-cols-12 pt-24 ${className}`} data-testid={testId}>
      <section className="col-span-10 col-start-2">
        <AccordionGroup>
          {/* 其他Accordion... */}
          <Accordion 
            className="lg:hidden" 
            title="Portfolio" 
            ref={portfolioAccordionRef}
            onContentHeightChange={updatePortfolioAccordionHeight}
          >
            <Portfolio />
          </Accordion>
          {/* 其他Accordion... */}
        </AccordionGroup>
      </section>
    </main>
  )
}
  1. 最后在Accordion.tsx中用useImperativeHandle暴露更新高度的方法:
// 引入forwardRef和useImperativeHandle
import React, { forwardRef, useImperativeHandle, ... } from "react"

// 修改组件为forwardRef
export const Accordion = forwardRef<{ updateHeight: () => void }, AccordionProps>(({
  className = "",
  title,
  children,
  isActive,
  onClick,
  headingSize = "h1",
  testId = "Accordion",
  onContentHeightChange
}, ref) => {
  // 原有代码...

  // 暴露更新高度的方法给父组件
  useImperativeHandle(ref, () => ({
    updateHeight: () => {
      if (!contentHeight.current) return
      if (isOpen || (isActive !== undefined && isActive)) {
        setHeight(`${contentHeight.current.scrollHeight}px`)
      }
    }
  }))

  // 原有handleToggle和返回JSX...
})

这个方案需要手动传递回调和处理ref,相对麻烦一些,但适合需要更精确控制高度更新时机的场景。


小提示

记得测试一下两种方案,我个人更推荐方案一,因为它不需要修改组件层级结构,也不需要手动传递回调,完全自动处理高度变化,代码更简洁维护性更好。

备注:内容来源于stack exchange,提问作者pyan

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.22 07:59:37