facebook/react-native

FlatList item onPress not work the first time after refreshed

Closed this issue Β· 50 comments

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>
    );
  }
}

Fascinating. Does it work without a refresh control? What about a touchablewithoutfeedback instead of a touchablehighlight?

  • If I remove refreshControl then auto refresh with a setTimeout then it works fine. Touches registered everytime.
  • It does NOT work with TouchableHighlight, TouchableOpacity or TouchableWithoutFeedback.

Update that Android doesn't has this bug.

Does this repro with v0.56.0?

Yes it still does.

// Updated first post with latest info from react-native info.

Same issue!

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>
    );
  }
}

Are there any workarounds for this? Invoke a fake scroll or touch event? Any more hints for how to go about fixing?

setState does not re-render nor update FlatList on iOS event with the extraData workaround any solution?

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

Thanks @rayhk6, this works fine for me !

Thanks @rayhk6, this works fine for me !

it's not perfect, it's will show after 200ms, not immediately

add "title" in RefreshControl, can be work in v0.56

1、 Enter the page
2、Click refresh is OK
3、Pull down to refresh is OK
4、Click refresh is invalid
As long as the drop-down refresh and click refresh will not work,But the pulldown refresh has been normal
image

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

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

onPress is normal,code is normal ,but refresh is not work

_refresh = () => {
        this.setState({
            refreshing: true,
        },() => {console.log(this.state.refreshing)});
        setTimeout(() => {
            this.getAssetDebtList(1)
        }, 1500)
    }
// log: true

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

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

for me too, tested on 0.57.7

Thanks everyone! While we agree that this repros, it would be great if someone wanted to investigate into the root cause. "Me too"s don't really help us solve this. πŸ˜•

Same issue here!! I stuck here for two days already!

react-native: 0.55.4
platform: only iOS.

1: try to add title for RefreshControl still cannot work. @binlaniua

Here is My Code:

`
'use strict';

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Platform, FlatList, FlatListProps, RefreshControl, ScrollView, View } from 'react-native';
import * as Colors from '../../constants/Colors';
import * as Dimens from '../../constants/Dimension';

export interface CommonFlatListProps extends FlatListProps {
    loadMoreMode: 'always' | 'never'
}

export default class CommonFlatList extends Component<CommonFlatListProps>{

    static propTypes = {
        renderItem: PropTypes.func.isRequired
        // Refer to the FlatList for the rest props
    }

    static defaultProps = {
        refreshing: false,
        keyExtractor: (item, index) => ('1000' + index),
        onEndReachedThreshold: 0.01, // don't change
        removeClippedSubviews: true,
        ItemSeparatorComponent: () => <View style={{ height: Dimens.line, backgroundColor: Colors.line }} />
    }

    constructor(props) {
        super(props);
        this.initLoadMore = false
        this.shouldLoadMore = false
    }
    
    scrollToEnd = () => {
        //params?: { animated?: boolean }
        this.list.scrollToEnd();
    }

    scrollToIndex = (params) => {
        //params: { animated?: boolean; index: number; viewOffset?: number; viewPosition?: number }
        this.list.scrollToIndex(params)
    }

    scrollToItem = (params) => {
        //params: { animated?: boolean; item: ItemT; viewPosition?: number }
        this.list.scrollToItem(params)
    }

    onScrollBeginDrag = ({ nativeEvent }) => {
        const { contentOffset: co, contentSize: cs, layoutMeasurement: ls } = nativeEvent
        if (co.y > 0 && (parseInt(co.y + ls.height) >= parseInt(cs.height))) {
            this.shouldLoadMore = true
        }
        this.props.onScrollBeginDrag && this.props.onScrollBeginDrag()
    }

    onScrollEndDrag = ({ nativeEvent }) => {
        const { loadMoreMode, onScrollEndDrag } = this.props
        const { contentOffset: co, contentSize: cs, layoutMeasurement: ls, velocity } = nativeEvent
        if (loadMoreMode == 'always') {
            if (this.shouldLoadMore && Platform.OS === 'android') {
                if (velocity.y < -1.2) {
                    this._onEndReached({}, true)
                }
            } else if (this.shouldLoadMore && parseInt(co.y + ls.height) > parseInt(cs.height + 30)) {
                this._onEndReached({}, true)
            }
        }
        this.shouldLoadMore = false
        onScrollEndDrag && onScrollEndDrag()
    }

    _onEndReached = (info, load) => {
        const { loadMoreMode, onEndReached } = this.props
        if (loadMoreMode == 'always' && load === true) {
            onEndReached && onEndReached(info, true, Platform.OS === 'android')
        } else if (Platform.OS === 'android' && !this.initLoadMore &&
            loadMoreMode == 'always' && info.distanceFromEnd < -1) {

            this.initLoadMore = true
            onEndReached && onEndReached(info, true, true)
        } else {
            onEndReached && onEndReached(info, false)
        }
    }

    _renderScrollComponent = (props) => {
        const { onRefresh, progressViewOffset } = props;
        if (onRefresh) {
            return <ScrollView
                {...props}
                refreshControl={
                    <RefreshControl
                        title='pull down to refresh'
                        colors={[Colors.baseColor]}
                        tintColor={Colors.baseColor}
                        refreshing={this.props.refreshing}
                        progressViewOffset={progressViewOffset}
                        onRefresh={onRefresh}
                    />
                }
            />
        } else {
            return <ScrollView {...props} />
        }
    }

    render() {
        return <FlatList
            {...this.props}
            ref={c => this.list = c}
            style={[{ backgroundColor: Colors.background }, this.props.style]}
            renderScrollComponent={this._renderScrollComponent}
            keyboardDismissMode='on-drag'
            onScrollBeginDrag={this.onScrollBeginDrag}
            onScrollEndDrag={this.onScrollEndDrag}
            onEndReached={this._onEndReached}
        />
    }
}

`

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

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

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?

Having the same issue setTimeout isn't the ideal solution. It is breaking the loading indicator animation.

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

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

me too ,0.61.4

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

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

This worked for me properly on a SectionList

Thanks a lot!

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

This worked for me

Thanks

disableScrollViewPanResponder = {true} fix the bug

Does anybody know what the consequences of using disableScrollViewPanResponer are? It all looks fine on first glance but I'm worried about any side effects down the line (performance, bugs, etc.)

On another note - I figured out that this bug doesn't occur when you swipe down and release quickly. It only happens if you swipe down and still hold your finger down after the list has refreshed. Hopefully this helps.

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

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

Aryk commented

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

I needed to increase the timeout to 400, but it doesn't appear to be working so far...

Still facing this issue on Android. Any solution for this? Even adding a setTimeout doesn't work for me

Hi all, I found the bug and the actual problem was with updating the redux state (there is some kind of delay here). If you're updating the state on press, please comment that code and check if it works for you. In my case I was updating the state on press and opening an animated view... This is a weird issue. I should find a way now to pass the updated state before displaying the animated view

The fix for this has been landed in #30291

As 0.64 has already been cut and is close to release I don't expect this to be included there. I expect this to be part of 0.65.

Infact you are the real Savior! After 2 years 😁

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

@ravirajn22 Thank you!
rn 0.63.4

if onPress is not workingin the flatlist's renderItem method try using onTouchStart method of the components if they have in the flatList's renderItem method.

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

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

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 !!!!

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

Not working when using a custom refreshControl

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

it worked here s2

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

Thank you, it works on RN 0.64

Hey guys, I managed to solve my problem by following this question https://stackoverflow.com/questions/68502633/react-native-why-is-my-flatlist-not-displaying

Props that I added to my flatList to work.

maxToRenderPerBatch={1000} windowSize={60} updateCellsBatchingPeriod={50} initialNumToRender={50}

Hope this helps you too <3

Hey guys, I managed to solve my problem by following this question https://stackoverflow.com/questions/68502633/react-native-why-is-my-flatlist-not-displaying

Props that I added to my flatList to work.

maxToRenderPerBatch={1000}
windowSize={60}
updateCellsBatchingPeriod={50}
initialNumToRender={50}

Hope this helps you too <3