Search code examples
react-nativereact-animatedreact-native-sectionlist

React native animated section list jumps to top when state/props change


I'm building a view in a react native app with a section list component and it also has a header above which will show aggregate data about the list. The header view should shrink down with animation when you scroll. I've got all the animation working fine, but issues arise when adding in infinite scrolling using the onEndreached prop. When new data is added or the state/props change, it jumps up to the top of the list.

To make the SectionList animate, I've wrapped it using Animated.createAnimatedComponent to create a <AnimatedSectionList /> component. It's when it's wrapped with Animated that the issues arise.

In the below-linked gif you can see that it jumps to the top of the list when I scroll down. This is when the state changes and we start fetching more data. https://monosnap.com/file/2aH5iPF2XcygEz381bpHW9QGPGHA2Z

I've tried using getItemLayout to calculate the list size hoping because I read that when react doesn't know the height of the list, it can be jumpy.

I've also tried scrolling to a section after new data is loaded using scrollTo but that doesn't work either. I was doing this with componentDidUpdate lifecycle method, but componentDidUpdate wasn't being called consistently when new data came in.

Here's my code.


const DataItem = [
  { name: "test name", value: "testvalue" },
  { name: "test name", value: "testvalue" },
  { name: "test name", value: "testvalue" },
  { name: "test name", value: "testvalue" },
  { name: "test name", value: "testvalue" }
];

const initialSections = [
  { title: "MY DETAILS", data: DataItem },
  { title: "MY COMMUNITY", data: DataItem },
  { title: "MY FAMILY", data: DataItem }
];

const styles = {
  item: {
    flex: 1,
    height: 50,
    marginTop: 1,
    backgroundColor: "#dddddd"
  },
  sectionHeader: {
    flex: 1,
    height: 20,
    marginTop: 1,
    backgroundColor: "#00ff00"
  }
};



class Edit extends Component {
  constructor(props) {
    super(props);

    this.state = {
      scrollEnabled: true,
      scrollY: new Animated.Value(0),
      sections: initialSections,
      refreshing: false
    };


  _renderSectionHeader(section) {
    return (
      <View style={styles.sectionHeader}>
        <Text> {section.title} </Text>
      </View>
    );
  }

  _renderItem(item) {
    return (
      <View style={styles.item}>
        <Text>{item.name}</Text>
      </View>
    );
  }


  handleStateButton() {
    this.setState({ refreshing: true });
  }

  fetchMoreData() {
    this.setState({ refreshing: true });

    const newData = [...this.state.sections, ...initialSections];

    setTimeout(
      function() {
        this.setState({ sections: newData, refreshing: false });       }.bind(this),
      2000
    );
  }

  render() {
    const backgroundScrollY = this.state.scrollY.interpolate({
      inputRange: [0, 224],
      outputRange: [0, -50],
      extrapolate: "clamp"
    });

    const backgroundScrollHeight = this.state.scrollY.interpolate({
      inputRange: [0, 224],
      outputRange: [1, 0.75],
      extrapolate: "clamp"
    });

    const listScrollY = this.state.scrollY.interpolate({
      inputRange: [0, 224],
      outputRange: [0, -10],
      extrapolate: "clamp"
    });

    const infoOpacity = this.state.scrollY.interpolate({
      inputRange: [0, 0.5, 150],
      outputRange: [1, 1, 0],
      extrapolate: "clamp"
    });

    const AnimatedSectionList = Animated.createAnimatedComponent(SectionList);

    return (
      <View
        style={{
          flex: 1,
          //   alignItems: "flex-start",
          flexDirection: "column"
        }}
      >
        <Animated.View
          style={[
            {
              position: "relative",
              // flex: 1,
              // alignSelf: "flex-start",
              top: 0,
              minHeight: 200,
              height: 300,
              backgroundColor: "#ddee99",
              border: "1px solid #0000FF",
              justifyContent: "center",
              alignItems: "center"
            },
            { transform: [{ scaleY: backgroundScrollHeight }] }
          ]}
        >
          <Animated.Image
            source={{
              uri:
                "https://images.unsplash.com/photo-1558901591-3a5f333830dd?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1350&q=80"
            }}
            style={[
              { position: "absolute", backgroundColor: "#cccccc" },
              { transform: [{ translateY: backgroundScrollY }] }
            ]}
            blurRadius={1.5}
          />

          <Text>{this.state.refreshing && `Fetching data`}</Text>

          <Button onPress={this.fetchMoreData} title="Get More Data" />
          <Button onPress={this.handleStateButton} title="Changing State" />
        </Animated.View>

        <AnimatedSectionList
          ref={ref => (this.sectionListRef = ref)}
          bounces={false}
          scrollEnabled={this.state.scrollEnabled}
          style={[
            { position: "relative" },
            { transform: [{ translateY: listScrollY }] }
          ]}
          onScroll={Animated.event(
            [{ nativeEvent: { contentOffset: { y: this.state.scrollY } } }],
            { listener: this._onScroll.bind(this) }
          )}
          sections={this.state.sections}
          renderSectionHeader={({ section }) =>
            this._renderSectionHeader(section)
          }
          ListHeaderComponent={() => (
            <View
              style={{
                flex: 1,
                flexDirection: "row",
                alignItems: "center",
                height: 40,
                backgroundColor: "#ff00ff"
              }}
            >
              <Text>List Header</Text>
            </View>
          )}
          ListFooterComponent={() => (
            <View
              style={{
                flex: 1,
                flexDirection: "row",
                alignItems: "center",
                height: 80,
                backgroundColor: "#FF0"
              }}
            >
              <Text>List Footer</Text>
            </View>
          )}
          getItemLayout={this.getItemLayout}
          renderItem={({ item }) => this._renderItem(item)}
          keyExtractor={(item, index) => index}
          stickySectionHeadersEnabled={true}
          onEndReachedThreshold={0.75}
          onEndReached={d => {
            this.fetchMoreData();
          }}
        />
      </View>
    );
  }
}


I would expect that the list would remain at the current scrolled position when the state or props change, but instead, it jumps to the top, defeating the purpose of infinite scroll.

Would appreciate any thoughts on the best way to approach this issue.


Solution

  • I put the

    const AnimatedSectionList = Animated.createAnimatedComponent(SectionList);
    

    out of the class and stopped jumping to the top