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在状态变化时通知父组件重新计算高度:
- 首先修改
AccordionProps,添加一个回调属性:
export interface AccordionProps extends Component { // 原有属性... onContentHeightChange?: () => void; // 新增回调 }
- 在
Accordion的handleToggle函数中,触发这个回调:
function handleToggle() { if (!contentHeight?.current) return const newIsOpen = !isOpen setIsOpen(newIsOpen) setHeight(newIsOpen ? `${contentHeight.current.scrollHeight}px` : "0px") onClick?.() // 通知父组件内容高度变化 onContentHeightChange?.() }
- 在父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> ) }
- 最后在
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

