Как получить накопленную высоту от дочерних элементов в родительском компоненте
Действительно извиняюсь за этот странно сформулированный вопрос. Я хочу создать короткую весеннюю анимацию с реагирующим движением. Пожалуйста, потерпите меня:
У меня есть компонент, который загружает список локальных каталогов и файлов внутри фрейма, немного похожий на окно поиска Mac, в соответствии с выбранным базовым каталогом. Таким образом, в зависимости от этого произвольного каталога, количество извлекаемых файлов будет варьироваться, и поэтому высота моего кадра будет изменяться (пока он не достигнет максимальной высоты).
Теперь, как только будет извлечено содержимое выбранного каталога, я хочу динамически открыть рамку этого компонента до нужной высоты (или максимальной высоты). И если бы мне пришлось сменить каталог, мне бы понадобилось, чтобы высота кадра была подпружинена / скорректирована в соответствии с количеством выбранных элементов ИЛИ прямо на максимальную высоту.
Вот соответствующая выдержка из компонента:
<Motion
defaultStyle={{x: 30}}
style={{x: spring(330, {stiffness: 270, damping: 17})}}
>
{({x}) =>
<List height={x}>
{
localTree.map(item =>
<ListItem key={v4()}>
<FileIcon isDirectory={item.isDirectory} />
<span onClick={() => this.handleClick(item.id)}>
{item.baseName}
</span>
</ListItem>
)
}
</List>
}
</Motion>
Вы можете видеть, что на данный момент defaultStyle
а также style
переменные статически установлены. Это работает нормально, особенно потому, что я установил <List />
стиль компонента в overflow: scroll
так что если есть больше элементов, чем 330px
в этом случае небольшая прокрутка делает его естественным для просмотра.
Но это не то, чего я в конечном итоге хочу. Я хочуdefaultStyle={{x: 30}}
а также style={{x: spring(330, )}}
быть динамически установленным по высоте накопленного числа <ListItem />
дочерние элементы.
Как я должен заняться этим? Должен ли я добавить родительский компонент где-нибудь?
1 ответ
Это очень интересный вопрос, очень куриный и яичный, как вы можете измерить высоту чего-то, что должно быть визуализировано, чтобы измерить это, без того, чтобы пользователь не увидел, что это сначала вспыхнуло. Плохие решения могут включать отображение чего-то за кадром и т. Д. И его измерение, но это не соответствует концепциям, которые продвигает React.
Однако ее можно решить, если понять не только жизненный цикл компонента, но и его соответствие жизненному циклу браузера.
Ключ к этому componentDidMount
а также componentDidUpdate
, Распространенным заблуждением является то, что это происходит после того, как компонент был отображен в браузере, это не так, он срабатывает непосредственно перед тем, как это происходит
render
сначала примирит виртуальный DOM со всем, что вы просили визуализировать (другое заблуждение здесь - виртуальный DOM на самом деле не является реальным или даже теневым DOM, это просто дерево объектов JS, представляющих компоненты и их DOM, которые алгоритм может получить отличается от - вы не можете определить динамические высоты или взаимодействовать с реальными элементами DOM в этой точке).
Если он чувствует, что существуют реальные мутации DOM, то componentDidMount
/componentDidUpdate
теперь запускается ДО фактического рендеринга браузера.
Затем он выполнит производные обновления для реального DOM (эффективно rendering
это к браузеру DOM). Важно отметить, что жизненный цикл браузера означает, что этот визуализированный DOM еще не отображается на экране (т.е. rendered
с точки зрения пользователя), до следующего тика, когда requestAnimationFrame
был шанс перехватить это...
componentWillReceiveProps
|
V
shouldComponentUpdate
|
V
componentWillUpdate
|
V
render
|
V
componentDidUpdate
|
V
[BROWSER DOM rendered - can be interacted with programatically]
|
V
requestAnimationFrame callback
|
V
[BROWSER screen painted with updated DOM - can be seen and interacted by user]
Так что на данный момент componentDidX
у нас есть возможность сказать ему перехватить браузер ДО того, как он нарисует, и все же мы можем взаимодействовать с реальным обновленным DOM и выполнять измерения! Для этого нам нужно использовать window.requestAnimationFrame
которая принимает функцию обратного вызова, в которой мы можем выполнить измерение. Поскольку это асинхронная функция, она возвращает идентификатор, который вы можете использовать для отмены, так же, как это делает setTimeout (и мы должны помнить, что нужно очистить все ожидающие запросы, так что componentWillUnmount
).
Нам также понадобится измеримый родительский элемент, для которого не установлены стили, с ref
хранится, чтобы мы могли добраться до узла DOM, чтобы измерить его. И обертка, которая будет анимирована по высоте.
Упрощенный без реакции-движения (но внешний div
можно завернуть в Motion
и может подключить интерполяцию к динамическому стилю опоры)...
// added type info for better clarity
constructor(props: SomeProps) {
super(props)
this.state = {
height: undefined
}
}
listRef: HTMLElement
bindListRef = (el: HTMLElement): void => {
this.listRef = el
}
rafId: number
interceptAndMeasure = (): void {
// use request animation frame to intercept and measure new DOM before the user sees it
this.rafId = window.requestAnimationFrame(this.onRaf)
}
onRaf = (): void => {
const height: number = this.listRef.getBoundingClientRect().height
// if we store this in the state - the screen will not get painted, as a new complete render lifecycle will ensue
// BEWARE you must have a guard condition against setting state unnecessarily here
// as it is triggered from within `componentDidX` which can if unchecked cause infinite re-render loops!
if (height !== this.state.height) this.setState({height})
}
render() {
const {data} = this.props
const {height} = this.state.height
// the ul will be pushed out to the height of its children, and we store a ref that we can measure it by
// the wrapper div can have its height dynamically set to the measured value (or an interpolated interim animated value!)
return (
<div style={height !== undefined ? {height: `${height}px`} : {}}>
<ul ref={this.bindListRef}>
{data.map((_, i) =>
<li key={i}>
{_.someMassiveText}
</li>
)}
</ul>
</div>
)
}
componentDidMount() {
this.interceptAndMeasure()
}
componentDidUpdate() {
this.interceptAndMeasure()
}
componentWillUnmount() {
// clean up in case unmounted while still pending
window.cancelAnimationFrame(this.rafId)
}
Конечно, с этим уже сталкивались раньше, и, если повезет, вы можете не беспокоиться о реализации этого самостоятельно и использовать react-collapse
пакет ( https://github.com/nkbt/react-collapse), который использует react-motion
под поверхностью, чтобы сделать именно это, а также обрабатывает много краевых случаев, таких как вложение динамической высоты и т. д.
Но прежде чем я это сделаю, я действительно призываю вас хотя бы попробовать это самостоятельно, так как это открывает гораздо более глубокое понимание жизненного цикла React и браузера, а также поможет с глубоко интерактивными возможностями, которых вам было бы трудно достичь в противном случае.