React+Vite环境下折叠面板状态不更新异常问题
问题分析与修复方案
核心问题
- DOM获取时机错误:组件初始化时直接调用
document.querySelectorAll(".collapseable"),此时组件还未渲染到DOM,得到的是空数组,导致状态数组长度为0,无法正确初始化面板收起状态,所以刷新后面板默认展开。 - 状态逻辑不符合需求:原
handleCollapse仅切换当前面板的状态,其他面板状态保持,无法实现“同一时间仅一个面板打开”的要求;且当状态数组长度异常时,点击会导致所有面板状态混乱。 - 直接操作DOM不符合React范式:通过
document.querySelector和手动修改style.height的方式,容易和React的状态更新机制冲突,导致交互异常。
修复后的代码
import React, { useState, useRef, useEffect } from "react"; import { facebookIcon, foursquareIcon, instagramIcon, lvLogo, pinterestIcon, plusIcon, snapchatIcon, twitterIcon, youtubeIcon, } from "../utils"; const FooterTop = () => { // 用单个状态存储当前打开的面板索引,null表示全关闭 const [openIndex, setOpenIndex] = useState(null); // 用ref存储所有面板元素,避免频繁查询DOM const collapseRefs = useRef([]); // 监听openIndex变化,更新面板高度 useEffect(() => { collapseRefs.current.forEach((panel, index) => { if (!panel) return; if (index === openIndex) { // 展开:设置为内容高度 panel.style.height = `${panel.scrollHeight}px`; } else { // 收起:设置为0 panel.style.height = "0px"; } }); }, [openIndex]); const handleCollapse = (index) => { // 点击已打开的面板则关闭,否则打开该面板并关闭其他 setOpenIndex(prev => prev === index ? null : index); }; return ( <> <div className="border-b"> <div className="mt-8 flex justify-center"> <img width={200} src={lvLogo} alt="LV Logo" /> </div> <div className="mt-8"> <div className="block border-t mx-5 py-4"> <button onClick={() => handleCollapse(0)} className="flex items-center justify-between w-full" > <div>Wünschen Sie Beratung?</div> <img width={10} src={plusIcon} alt="add" /> </button> <div ref={el => collapseRefs.current[0] = el} className="collapseable overflow-hidden transition-all duration-300" > <ul className="pl-5"> <li className="my-10"> <div> Kontaktieren Sie unseren Kundenservice gerne telefonisch unter{" "} <a className="underlined-link" href="#"> +49 (0) 211 864 700 </a> , per{" "} <a className="underlined-link" href="#"> E-mail </a>{" "} oder{" "} <a className="underlined-link" href="#"> Whatsapp </a> . </div> </li> <li className="my-10"> <a href="#">Häufige Frage (FAQ)</a> </li> <li className="my-10"> <a href="#">Reparatur</a> </li> <li className="my-5"> <a href="#">Geschäfte</a> </li> </ul> </div> </div> <div className="block border-t mx-5 py-4"> <button onClick={() => handleCollapse(1)} className="flex items-center justify-between w-full" > <div>Services</div> <img width={10} src={plusIcon} alt="add" /> </button> <div ref={el => collapseRefs.current[1] = el} className="collapseable overflow-hidden transition-all duration-300" > <ul className="pl-5"> <li className="my-10"> <a href="#">Care Service</a> </li> <li className="my-10"> <a href="#">Personalisierung</a> </li> <li className="my-5"> <a href="#">Die Kunst des Schenkens</a> </li> </ul> </div> </div> <div className="block border-t mx-5 py-4"> <button onClick={() => handleCollapse(2)} className="flex items-center justify-between w-full" > <div>Über Louis Vuitton</div> <img width={10} src={plusIcon} alt="add" /> </button> <div ref={el => collapseRefs.current[2] = el} className="collapseable overflow-hidden transition-all duration-300" > <ul className="pl-5"> <li className="my-10"> <a href="#">Fashion Shows</a> </li> <li className="my-10"> <a href="#">Kunst & Kultur</a> </li> <li className="my-10"> <a href="#">Maison Louis Vuitton</a> </li> <li className="my-10"> <a href="#">Nachhaltigkeit</a> </li> <li className="my-10"> <a href="#">Neuigkeiten</a> </li> <li className="my-10"> <a href="#">Karriere</a> </li> <li className="my-5"> <a href="#">Fondation Louis Vuitton</a> </li> </ul> </div> </div> <div className="block border-t mx-5 py-4"> <button onClick={() => handleCollapse(3)} className="flex items-center justify-between w-full" > <div>Folgen Sie uns</div> <img width={10} src={plusIcon} alt="add" /> </button> <div ref={el => collapseRefs.current[3] = el} className="collapseable overflow-hidden transition-all duration-300" > <ul className="pl-5"> <li className="my-10"> Abbonieren Sie den{" "} <a className="underlined-link" href="#"> Newsletter von Louis Vuitton </a>{" "} und erfahren Sie zuerst von unseren neuesten Kollektionen, Kampagnen und Initiativen. </li> <li className="my-10"> <a href="#">Apps</a> </li> <li className="flex my-10 justify-center gap-7"> <a href="#"> <img width={25} src={instagramIcon} alt="Instagram" /> </a> <a href="#"> <img width={25} src={facebookIcon} alt="Facebook" /> </a> <a href="#"> <img width={25} src={twitterIcon} alt="X" /> </a> <a href="#"> <img width={25} src={youtubeIcon} alt="YouTube" /> </a> <a href="#"> <img width={25} src={snapchatIcon} alt="Snapchat" /> </a> <a href="#"> <img width={25} src={pinterestIcon} alt="Pinterest" /> </a> <a href="#"> <img width={25} src={foursquareIcon} alt="Foursquare" /> </a> </li> </ul> </div> </div> </div> </div> </> ); }; export default FooterTop;
关键优化点
- 状态简化:用
openIndex单个状态替代数组,更直观地控制唯一打开的面板,初始值null确保所有面板默认收起。 - 正确获取DOM元素:使用
useRef存储每个面板元素,避免在组件初始化时查询未渲染的DOM,同时减少DOM查询次数。 - 交互逻辑修正:点击按钮时,若当前面板已打开则关闭,否则打开该面板并自动关闭其他面板,完全符合“同一时间仅一个面板打开”的需求。
- 符合React范式:通过状态驱动DOM样式变化,避免直接操作DOM导致的状态不一致问题。
内容的提问来源于stack exchange,提问作者EnXan
相关产品推荐
相关产品推荐

