如何在React中获取SelectedCard渲染完成后的准确高度?
问题描述
我需要在SelectedCard组件完全渲染完成后获取其高度,最多会渲染3个该组件,最终取它们的高度最大值。组件渲染过程中会显示骨架屏,但目前获取的height值不稳定——单组件测试时正确高度应为160,但偶尔会拿到39或85,应该是捕获了骨架屏或渲染中的高度。
试过useEffect,结果一样;在useLayoutEffect里加6秒定时器能稳定拿到正确值,但会导致样式延迟偏移,而且固定等待时间完全不可靠。现在需要一个不用定时器、能稳定获取准确高度的方案。
现有相关代码:
useLayoutEffect(() => { setTimeout(() => { let height = 0; setMinHeight(height); cardRefs.current.forEach(ref => { if (ref) { height = Math.max(height, ref.clientHeight); } }); console.log(`ktest: maxHeight: ${height}`) setMinHeight(height); }, 6000) }, [comparedCardsData, numberOfColumns]);
import React, {useLayoutEffect, useRef, useState,} from 'react'; import {compose} from 'redux'; import {connect} from 'react-redux'; import SelectedCard from './SelectedCard'; import {createStructuredSelector} from "reselect"; const Header1 = ({ data, numberOfColumns, }) => { const [minHeight, setMinHeight] = useState(0); const cardRefs = useRef([]); useLayoutEffect(() => { let height = 0; cardRefs.current.forEach(ref => { if (ref) { height = Math.max(height, ref.clientHeight); } }); console.log(`height: ${height}`) // differs intermittently. setMinHeight(height); }, [data, numberOfColumns]); return data.slice(0, numberOfColumns) .map((card, index) => { return ( <div> <div ref={ref => cardRefs.current[index] = ref} style={{minHeight}}> <SelectedCard/> </div> {/* many other components */} </div> ); }); }; const mapStateToProps = createStructuredSelector({ ..... }); const mapDispatchToProps = { ..... }; const all = compose( connect(mapStateToProps, mapDispatchToProps) ); export default all(Header1);
解决方案
方案1:让SelectedCard主动通知父组件渲染完成
核心思路是让子组件在自身完全渲染(骨架屏消失、真实内容加载完毕)后,主动向父组件传递自身高度,父组件收集所有子组件的高度后计算最大值。
父组件修改
import React, {useRef, useState,} from 'react'; import {compose} from 'redux'; import {connect} from 'react-redux'; import SelectedCard from './SelectedCard'; import {createStructuredSelector} from "reselect"; const Header1 = ({ data, numberOfColumns, }) => { const [minHeight, setMinHeight] = useState(0); // 存储已获取的卡片高度,key为索引,value为高度 const cardHeights = useRef({}); // 接收子组件传递的高度 const handleCardReady = (index, height) => { cardHeights.current[index] = height; // 收集到所有卡片高度后计算最大值 const heights = Object.values(cardHeights.current); if (heights.length === numberOfColumns) { const maxHeight = Math.max(...heights); setMinHeight(maxHeight); } }; return data.slice(0, numberOfColumns) .map((card, index) => { return ( <div key={index}> <div style={{minHeight}}> <SelectedCard onReady={(height) => handleCardReady(index, height)} /> </div> {/* many other components */} </div> ); }); }; // 以下原有代码不变 const mapStateToProps = createStructuredSelector({ ..... }); const mapDispatchToProps = { ..... }; const all = compose( connect(mapStateToProps, mapDispatchToProps) ); export default all(Header1);
子组件SelectedCard修改
在SelectedCard内部,当骨架屏消失、真实内容渲染完成后,通过props回调把自身高度传给父组件:
// SelectedCard.jsx示例 import React, {useRef, useEffect, useState} from 'react'; const SelectedCard = ({onReady}) => { const cardRef = useRef(null); const [isLoaded, setIsLoaded] = useState(false); // 替换为你真实的数据加载/骨架屏切换逻辑 useEffect(() => { fetchYourData().then(() => { setIsLoaded(true); }); }, []); // 加载完成后通知父组件高度 useEffect(() => { if (isLoaded && cardRef.current && onReady) { const height = cardRef.current.clientHeight; onReady(height); } }, [isLoaded, onReady]); return ( <div ref={cardRef}> {isLoaded ? ( // 真实内容区 <div>你的卡片内容</div> ) : ( // 骨架屏区 <div className="skeleton">骨架屏内容</div> )} </div> ); }; export default SelectedCard;
方案2:用ResizeObserver监听高度变化(无需修改子组件)
如果无法修改SelectedCard组件,可以用ResizeObserver监听父容器的高度变化,直到高度稳定(不再变化)时再设置minHeight。
import React, {useRef, useState, useEffect} from 'react'; import {compose} from 'redux'; import {connect} from 'react-redux'; import SelectedCard from './SelectedCard'; import {createStructuredSelector} from "reselect"; const Header1 = ({ data, numberOfColumns, }) => { const [minHeight, setMinHeight] = useState(0); const cardRefs = useRef([]); // 存储每个卡片的最后一次高度,用于判断是否稳定 const lastHeights = useRef({}); const resizeObservers = useRef([]); useEffect(() => { // 清空之前的监听 resizeObservers.current.forEach(observer => observer.disconnect()); resizeObservers.current = []; lastHeights.current = {}; data.slice(0, numberOfColumns).forEach((_, index) => { const ref = cardRefs.current[index]; if (!ref) return; const observer = new ResizeObserver((entries) => { const currentHeight = entries[0].contentRect.height; // 高度两次一致则判定为稳定 if (lastHeights.current[index] === currentHeight) { lastHeights.current[index] = currentHeight; // 检查所有卡片是否都稳定 const allStable = Object.keys(lastHeights.current).length === numberOfColumns; if (allStable) { const maxHeight = Math.max(...Object.values(lastHeights.current)); setMinHeight(maxHeight); // 稳定后断开监听,减少不必要触发 observer.disconnect(); } } else { lastHeights.current[index] = currentHeight; } }); observer.observe(ref); resizeObservers.current.push(observer); }); // 组件卸载时清理监听 return () => { resizeObservers.current.forEach(observer => observer.disconnect()); }; }, [data, numberOfColumns]); return data.slice(0, numberOfColumns) .map((card, index) => { return ( <div key={index}> <div ref={ref => cardRefs.current[index] = ref} style={{minHeight}}> <SelectedCard/> </div> {/* many other components */} </div> ); }); }; // 以下原有代码不变 const mapStateToProps = createStructuredSelector({ ..... }); const mapDispatchToProps = { ..... }; const all = compose( connect(mapStateToProps, mapDispatchToProps) ); export default all(Header1);
方案3:依赖SelectedCard的加载状态同步计算
如果SelectedCard的加载状态可以通过props传递给父组件,父组件可以在所有卡片加载完成后再计算高度:
import React, {useRef, useState, useEffect} from 'react'; import {compose} from 'redux'; import {connect} from 'react-redux'; import SelectedCard from './SelectedCard'; import {createStructuredSelector} from "reselect"; const Header1 = ({ data, numberOfColumns, }) => { const [minHeight, setMinHeight] = useState(0); const cardRefs = useRef([]); // 跟踪每个卡片的加载状态 const [cardLoadedStatus, setCardLoadedStatus] = useState({}); // 所有卡片加载完成后计算高度 useEffect(() => { const allLoaded = Object.values(cardLoadedStatus).every(status => status); if (allLoaded) { let maxHeight = 0; cardRefs.current.forEach(ref => { if (ref) { maxHeight = Math.max(maxHeight, ref.clientHeight); } }); setMinHeight(maxHeight); } }, [cardLoadedStatus]); return data.slice(0, numberOfColumns) .map((card, index) => { return ( <div key={index}> <div ref={ref => cardRefs.current[index] = ref} style={{minHeight}}> <SelectedCard onLoadComplete={() => { setCardLoadedStatus(prev => ({...prev, [index]: true})); }} /> </div> {/* many other components */} </div> ); }); }; // 以下原有代码不变 const mapStateToProps = createStructuredSelector({ ..... }); const mapDispatchToProps = { ..... }; const all = compose( connect(mapStateToProps, mapDispatchToProps) ); export default all(Header1);
内容的提问来源于stack exchange,提问作者karvai
相关产品推荐
相关产品推荐

