react-native-collapsible icon indicating copy to clipboard operation
react-native-collapsible copied to clipboard

Collapsible does not rerender properly

Open AizenSousuke opened this issue 4 years ago • 7 comments

Basically I have a parent component that passes data to a child component within a Collapsible as such:

			<Collapsible collapsed={isCollapsed}>
				<BusStop busStopData={busStopData} />
			</Collapsible>

On busStopData changed, the component doesn't rerender properly i.e, blank text for those properties of busStopData which were changed. When I removed the Collapsible component, the BusStop component rerenders correctly.

AizenSousuke avatar Jan 03 '21 05:01 AizenSousuke

Any workaround for this? Looks like this repo is not being maintained.

vishalmanohar avatar Feb 18 '21 13:02 vishalmanohar

no workaround so far

AizenSousuke avatar Feb 22 '21 05:02 AizenSousuke

Same here. Spent a whole day trying to make this package work. They should at least put a deprecated notice.

wmonecke avatar Mar 20 '21 19:03 wmonecke

Hi, I tried to reproduce this bug but I'm not able to. Can you provide a minimal example that I can investigate?

oblador avatar Apr 29 '21 17:04 oblador

@oblador I found that adding flex: 1 to a child component could introduce flickering or height measurements bugs.

I improved on the implementation of Collapsible with reanimated. It has been working without issue on a 30K MAU app.

Do you think we could migrate this package to reanimated?

This is my implementation:

import PropTypes from 'prop-types'
import React, { Component } from 'react'
import { ViewPropTypes } from './config'
import Animated, { Easing } from 'react-native-reanimated'

export default class Collapsible extends Component {
  static propTypes = {
    children: PropTypes.node,
    collapsed: PropTypes.bool,
    duration: PropTypes.number,
    style: ViewPropTypes.style,
    onAnimationEnd: PropTypes.func,
    collapsedHeight: PropTypes.number,
    enablePointerEvents: PropTypes.bool,
    align: PropTypes.oneOf(['top', 'center', 'bottom']),
    easing: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
  }

  static defaultProps = {
    align: 'top',
    duration: 120,
    collapsed: true,
    collapsedHeight: 0,
    easing: 'easeOutCubic',
    enablePointerEvents: false,
    onAnimationEnd: () => null,
  }

  constructor(props) {
    super(props)
    this.state = {
      measured: false,
      animating: false,
      measuring: false,
      contentHeight: 0,
      height: new Animated.Value(props.collapsedHeight),
    }
  }

  componentDidUpdate(prevProps) {
    if (prevProps.collapsed !== this.props.collapsed) {
      this.setState({ measured: false }, () => this._componentDidUpdate(prevProps))
    } else {
      this._componentDidUpdate(prevProps)
    }
  }

  componentWillUnmount() {
    this.unmounted = true
  }

  _componentDidUpdate(prevProps) {
    if (prevProps.collapsed !== this.props.collapsed) {
      this._toggleCollapsed(this.props.collapsed)
    } else if (this.props.collapsed && prevProps.collapsedHeight !== this.props.collapsedHeight) {
      this.state.height.setValue(this.props.collapsedHeight)
    }
  }

  contentHandle = null

  _handleRef = ref => {
    this.contentHandle = ref
  }

  _measureContent(callback) {
    this.setState(
      {
        measuring: true,
      },
      () => {
        requestAnimationFrame(() => {
          if (!this.contentHandle) {
            this.setState(
              {
                measuring: false,
              },
              () => callback(this.props.collapsedHeight),
            )
          } else {
            this.contentHandle.getNode().measure((x, y, width, height) => {
              this.setState(
                {
                  measuring: false,
                  measured: true,
                  contentHeight: height,
                },
                () => callback(height),
              )
            })
          }
        })
      },
    )
  }

  _toggleCollapsed(collapsed) {
    if (collapsed) {
      this._transitionToHeight(this.props.collapsedHeight)
    } else if (!this.contentHandle) {
      if (this.state.measured) {
        this._transitionToHeight(this.state.contentHeight)
      }
      return
    } else {
      this._measureContent(contentHeight => {
        this._transitionToHeight(contentHeight)
      })
    }
  }

  _transitionToHeight(height) {
    const configAnimIn = {
      toValue: height,
      duration: 150,
      easing: Easing.inOut(Easing.ease),
    }

    const configAnimOut = {
      toValue: 0,
      duration: 150,
      easing: Easing.inOut(Easing.ease),
    }

    this._animIn = Animated.timing(this.state.height, configAnimIn)
    this._animOut = Animated.timing(this.state.height, configAnimOut)
    this.setState({ animating: true })
    if (height === 0) {
      this._animOut.start(({ finished }) => {
        if (finished) {
          this.setState({ animating: false })
        }
      })
    } else {
      this._animIn.start(({ finished }) => {
        if (finished) {
          this.setState({ animating: false })
        }
      })
    }
  }

  _handleLayoutChange = event => {
    const contentHeight = event.nativeEvent.layout.height
    if (
      this.state.animating ||
      this.props.collapsed ||
      this.state.measuring ||
      this.state.contentHeight === contentHeight
    ) {
      return
    }

    this.state.height.setValue(contentHeight)
    this.setState({ contentHeight })
  }

  render() {
    const { collapsed, enablePointerEvents } = this.props
    const { height, measuring, measured } = this.state
    const hasKnownHeight = !measuring && (measured || collapsed)
    const style = hasKnownHeight && {
      overflow: 'hidden',
      height: height,
    }
    const contentStyle = {}
    if (measuring) {
      contentStyle.position = 'absolute'
      contentStyle.opacity = 0
    }
    return (
      <Animated.View style={style} pointerEvents={!enablePointerEvents && collapsed ? 'none' : 'auto'}>
        <Animated.View
          ref={this._handleRef}
          style={[this.props.style, contentStyle]}
          onLayout={this.state.animating ? undefined : this._handleLayoutChange}
        >
          {this.props.children}
        </Animated.View>
      </Animated.View>
    )
  }
}

wmonecke avatar Apr 29 '21 18:04 wmonecke

@wmonecke Hi, unfortunately your code introduces regressions. I found that there is actually very little changes one has to do to make it work. Would you be able to try this PR out for me in your project? https://github.com/oblador/react-native-collapsible/pull/401

oblador avatar Apr 30 '21 08:04 oblador

I solved it by not using {flex: 1} in the children of the Collapsible.

AizenSousuke avatar Aug 12 '21 13:08 AizenSousuke