react-native: FlatList item onPress not work the first time after refreshed

Environment

  React Native Environment Info:
    System:
      OS: macOS High Sierra 10.13.5
      CPU: x64 Intel(R) Core(TM) i5-5257U CPU @ 2.70GHz
      Memory: 25.63 MB / 8.00 GB
      Shell: 3.2.57 - /bin/bash
    Binaries:
      Node: 8.11.2 - /usr/local/bin/node
      Yarn: 1.7.0 - /usr/local/bin/yarn
      npm: 5.6.0 - /usr/local/bin/npm
      Watchman: 4.9.0 - /usr/local/bin/watchman
    SDKs:
      iOS SDK:
        Platforms: iOS 11.4, macOS 10.13, tvOS 11.4, watchOS 4.3
    IDEs:
      Android Studio: 3.1 AI-173.4819257
      Xcode: 9.4.1/9F2000 - /usr/bin/xcodebuild
    npmPackages:
      react: 16.3.1 => 16.3.1
      react-native: 0.56.0 => 0.56.0
    npmGlobalPackages:
      create-react-native-app: 1.0.0
      react-native-cli: 2.0.1
      react-native-scripts: 1.14.0

Description

FlatList has item with TouchableHighlight, and a RefreshControl attached. onPress method of TouchableHighlight is not working the first time after onRefresh called.

flatlist_bug_report

If I scroll FlatList a bit after refreshed, then item onPress works fine. // UPDATE: Android does not have this bug.

Reproducible Demo

Fresh project created by react-native init

import React, { Component } from "react";
import { Text, View, FlatList, TouchableOpacity, RefreshControl } from "react-native";

type Props = {};
export default class App extends Component<Props> {
  constructor() {
    super();
    this.state = { refreshing: true, items: [] };
  }

  componentDidMount() {
    this.refresh();
  }

  genItems = () => [0, 1, 2, 3, 4, 5];

  refresh = () => {
    this.setState({ refreshing: true, items: [] });
    setTimeout(() => this.setState({ refreshing: false, items: this.genItems() }), 1500);
  };

  renderItem = ({ item }) => {
    const text = `${item}`;
    return (
      <TouchableOpacity onPress={() => alert("pressed!")}>
        <Text style={{ width: "100%", height: 48, backgroundColor: "white" }}>
          {text}
        </Text>
        <View style={{ width: "100%", height: 1, backgroundColor: "gray" }} />
      </TouchableOpacity>
    );
  };

  render() {
    return (
      <View style={{ flex: 1, padding: 48 }}>
        <FlatList style={{ flex: 1, backgroundColor: "#aaa", borderColor: "gray", borderWidth: 1 }}
          renderItem={this.renderItem}
          data={this.state.items}
          keyExtractor={item => `${item}`}
          refreshControl={
            <RefreshControl
              refreshing={this.state.refreshing}
              onRefresh={this.refresh}
            />
          }
        />
      </View>
    );
  }
}

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Reactions: 20
  • Comments: 50 (6 by maintainers)

Commits related to this issue

Most upvoted comments

Same issue!

@ravirajn22 , thanks! Passing disableScrollViewPanResponder prop to my FlatList fixed the bug.

The following are the details I found by debugging, may be some one more familiar with reactnative event plugin could take it further.

This reproes with ScrollView, not just FlatList.

  • It requires clearing the items and then rendering them. Just triggering a state update with the same items isn’t enough.
  • It requires a refresh control and triggering onRefresh that way. If you do a setInterval in the componentDidMount which calls refresh(), that doesn’t repro.

Someone will need to dig into the ScrollView implementation and see if something funky is happening with the refresh control. It could also be somewhere in the ScrollableMixin or something like that.

This is the example I used:

class App extends React.Component<{}> {
  constructor() {
    super();
    this.state = {refreshing: true, items: []};
  }

  componentDidMount() {
    this.refresh();
  }

  refresh = () => {
    this.setState({
      refreshing: true,
      items: [],
    });

    setTimeout(
      () =>
        this.setState({
          refreshing: false,
          items: [0, 1, 2, 3, 4, 5],
        }),
      1500,
    );
  };

  renderItem = ({item}) => {
    return (
      <TouchableOpacity onPress={() => alert('pressed!')} key={`${item}`}>
        <Text style={{width: '100%', height: 48, backgroundColor: 'white'}}>
          {item}
        </Text>
        <View style={{width: '100%', height: 1, backgroundColor: 'gray'}} />
      </TouchableOpacity>
    );
  };

  render() {
    return (
      <View style={{flex: 1, padding: 48}}>
        <ScrollView
          style={{
            flex: 1,
            backgroundColor: '#aaa',
            borderColor: 'gray',
            borderWidth: 1,
          }}
          keyExtractor={item => `${item}`}
          refreshControl={
            <RefreshControl
              refreshing={this.state.refreshing}
              onRefresh={this.refresh}
            />
          }>
          {this.state.items.map(item => this.renderItem({item}))}
        </ScrollView>
      </View>
    );
  }
}

The issue happens because responderInst is still kept hold by the ScrollView after all the events are fired whereas in case where items: [] is commented in the setState, the responderInst is correctly set to null.

responderInst is a react component which will get all the touch events, how it works and set can be found in setResponderAndExtractTransfer in ReactNativeRenderer-dev.js.

targetInst is the react component on which the original touch happened.

Nesting of components is like this View -> ScrollView -> (View -> Text ) * multiplied by number of list items

There are lots of events fired in following order

  1. topTouchStart (targetInst = RCTText of the list item)
  2. topTouchMove (Many) (targetInst = RCTText of the list item)
  3. topScrollBeginDrag (targetInst = ScrollView)
  4. topScroll (targetInst = ScrollView)
  5. topTouchMove (Many) (targetInst = RCTText of the list item)
  6. topRefresh (targetInst = RCTRefreshController)
  7. topTouchMove (Many) (targetInst = RCTText of the list item)
  8. topTouchEnd (here is where the problem happens, targetInst = null since the list item are cleared)
  9. topScrollEndDrag (targetInst = ScrollView)
  10. topMomentumScrollBegin (targetInst = ScrollView)
  11. topScroll (targetInst = ScrollView)
  12. topMomentumScrollEnd (targetInst = ScrollView)
  13. topScroll (targetInst = ScrollView)
  14. topMomentumScrollEnd (targetInst = ScrollView)

The partial flow is when we pull to refresh, topTouchStart event is fired which calls ScrollView’s scrollResponderHandleTouchStart which sets isTouching to true.

  /**
   * Invoke this from an `onTouchStart` event.
   *
   * Since we know that the `SimpleEventPlugin` occurs later in the plugin
   * order, after `ResponderEventPlugin`, we can detect that we were *not*
   * permitted to be the responder (presumably because a contained view became
   * responder). The `onResponderReject` won't fire in that case - it only
   * fires when a *current* responder rejects our request.
   *
   * @param {PressEvent} e Touch Start event.
   */
  scrollResponderHandleTouchStart: function(e: PressEvent) {
    this.state.isTouching = true;
    this.props.onTouchStart && this.props.onTouchStart(e);
  },

isTouching determines if the ScrollView wants to become responderInst when topScroll event is fired

  /**
   * Invoke this from an `onScroll` event.
   */
  scrollResponderHandleScrollShouldSetResponder: function(): boolean {
    // Allow any event touch pass through if the default pan responder is disabled
    if (this.props.disableScrollViewPanResponder === true) {
      return false;
    }
    return this.state.isTouching;
  },

isTouching is set to false inside scrollResponderHandleTouchEnd when onTouchEnd event is fired . In our case this function is never called. Because ReactNativeBridgeEventPlugin’s extractEvents which determines which listeners (on the component) to call depends on targetInst. Since we set items=[] in setState the targetInst becomes null and none of our listeners in ScrollVIew (ScrollView is the parent of items, since items is null we cannot know its parents now) are called after the items are cleared. Hence when onTouchEnd is fired scrollResponderHandleTouchEnd of ScrollView is not called.

  /**
   * Invoke this from an `onTouchEnd` event.
   *
   * @param {PressEvent} e Event.
   */
  scrollResponderHandleTouchEnd: function(e: PressEvent) {
    const nativeEvent = e.nativeEvent;
    this.state.isTouching = nativeEvent.touches.length !== 0;
    this.props.onTouchEnd && this.props.onTouchEnd(e);
  },

Hope someone familiar with ScrollView responder system and react event system can take it further.

EDIT 1: In ScrollView setting disableScrollViewPanResponder=true will prevent this bug from happening, since it will prevent the ScrollView to become responder. But don’t use this, since I don’t know what regression it creates. Only purpose I added is for documenting.

EDIT 2: Tagging people who might know about this, @shergin

@Rahulnagarwal perfect! import { TouchableOpacity } from 'react-native-gesture-handler'; it worked here for me

I’ve been stuck with a similar issue I don’t know if its related or not. I’m developing for android only using a ScrollView and on a real device I can click buttons, they respond to being touched with the downstate etc but onPress doesn’t trigger. disableScrollViewPanResponder = {true} did not help in this case

I recognized that not only FlatList item, but every single Touchable on the screen won’t call onPress after FlatList refreshed.

I used a setTimeout to solve the issue <RefreshControl refreshing={this.state.refresh} onRefresh={() => setTimeout(() => { this.refeshAction() }, 200) } title="Test />

Same issue. Also setTimeout doesn’t work for me because of the breaking animation. Somebody has a better solution?

Same issue just push this inside the flatList onRefresh={() => setTimeout(() => { this.refeshAction() }, 200) } is working for me !!

try import { TouchableOpacity } from ‘react-native-gesture-handler’; It’s working properly

Same issue. Every touchable item on screen is not clickable after refresh

I 'm having this issue without the refresh on version 0.59.6

<FlatList
    style={styles.list}
    data={this.state.items}
    keyExtractor={(item) => `${item.id}`}
    renderItem={({item}) => (
        <TouchableOpacity onPress={() => this.onClickItem(item)} activeOpacity={0.8}>
            <CardView
                cardElevation={1}
                cornerRadius={5}
                style={stryles.card}>
                <View style={stryles.cardView}>
                    <Icon active name='directions-car' style={stryles.icon} />
                    <Text>{item.name}</Text>
                </View>
            </CardView>
        </TouchableOpacity>
    )}/>

Any workarounds or news on a fix?

This reproes with ScrollView, not just FlatList.

  • It requires clearing the items and then rendering them. Just triggering a state update with the same items isn’t enough.
  • It requires a refresh control and triggering onRefresh that way. If you do a setInterval in the componentDidMount which calls refresh(), that doesn’t repro.

Someone will need to dig into the ScrollView implementation and see if something funky is happening with the refresh control. It could also be somewhere in the ScrollableMixin or something like that.

This is the example I used:

class App extends React.Component<{}> {
  constructor() {
    super();
    this.state = {refreshing: true, items: []};
  }

  componentDidMount() {
    this.refresh();
  }

  refresh = () => {
    this.setState({
      refreshing: true,
      items: [],
    });

    setTimeout(
      () =>
        this.setState({
          refreshing: false,
          items: [0, 1, 2, 3, 4, 5],
        }),
      1500,
    );
  };

  renderItem = ({item}) => {
    return (
      <TouchableOpacity onPress={() => alert('pressed!')} key={`${item}`}>
        <Text style={{width: '100%', height: 48, backgroundColor: 'white'}}>
          {item}
        </Text>
        <View style={{width: '100%', height: 1, backgroundColor: 'gray'}} />
      </TouchableOpacity>
    );
  };

  render() {
    return (
      <View style={{flex: 1, padding: 48}}>
        <ScrollView
          style={{
            flex: 1,
            backgroundColor: '#aaa',
            borderColor: 'gray',
            borderWidth: 1,
          }}
          keyExtractor={item => `${item}`}
          refreshControl={
            <RefreshControl
              refreshing={this.state.refreshing}
              onRefresh={this.refresh}
            />
          }>
          {this.state.items.map(item => this.renderItem({item}))}
        </ScrollView>
      </View>
    );
  }
}

It wasn’t me at all, it was all @yum650350. This was their first contribution to React Native!

same bug here with the simple FlatList “react-native”: “0.62.2”

<ScrollView disableScrollViewPanResponder={true} refreshControl={ <RefreshControl refreshing={this.state.refreshing} onRefresh={this.onRefresh.bind(this)} /> } >

This worked for me

Thanks

@ravirajn22 , thanks! Passing disableScrollViewPanResponder prop to my FlatList fixed the bug.

This worked for me properly on a SectionList

Thanks a lot!

me too ,0.61.4

I don’t understand why this hasn’t been fixed yet. It only happens on Android

I’m really struggling with that. I don’t use Refreshcontrol but rather remove an item from the array.

Here my setup

"react": "^17.0.2",
"react-native": "^0.66.3",

Here the process

  1. Load data (array) from API into a hook
  2. Display the hook data in FlatList (data={data})
  3. The renderItem is wrapped in a TouchableOpacity
  4. By pressing on the renderItem an ID gets stored in the hook
  5. After invoking the delete function the original data gets filtered (new array) and stored in the hook data
  6. The FlatList re-renders and removes the item
  7. After that I can’t press the item anymore (onPress doesn’t get invoked)

I tried import { TouchableOpacity } from 'react-native-gesture-handler' as @souzaluiz suggested but it has no effect.

Please help !!!

disableScrollViewPanResponder = {true} fix the bug

The workaround setTimeout with 200ms delay works for me, but the refresh indicator has some animation problem.