FlatList item onPress not work the first time after refreshed
Closed this issue Β· 50 comments
- Review the documentation: https://facebook.github.io/react-native
- Search for existing issues: https://github.com/facebook/react-native/issues
- Use the latest React Native release: https://github.com/facebook/react-native/releases
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.
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 asetTimeout
then it works fine. Touches registered everytime. - It does NOT work with
TouchableHighlight
,TouchableOpacity
orTouchableWithoutFeedback
.
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 !
it's not perfect, it's will show after 200ms, not immediately
add "title" in RefreshControl, can be work in v0.56
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 singleTouchable
on the screen won't callonPress
afterFlatList
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
- topTouchStart (targetInst = RCTText of the list item)
- topTouchMove (Many) (targetInst = RCTText of the list item)
- topScrollBeginDrag (targetInst = ScrollView)
- topScroll (targetInst = ScrollView)
- topTouchMove (Many) (targetInst = RCTText of the list item)
- topRefresh (targetInst = RCTRefreshController)
- topTouchMove (Many) (targetInst = RCTText of the list item)
- topTouchEnd (here is where the problem happens, targetInst = null since the list item are cleared)
- topScrollEndDrag (targetInst = ScrollView)
- topMomentumScrollBegin (targetInst = ScrollView)
- topScroll (targetInst = ScrollView)
- topMomentumScrollEnd (targetInst = ScrollView)
- topScroll (targetInst = ScrollView)
- 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 myFlatList
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
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
- Load data (array) from API into a hook
- Display the hook data in
FlatList
(data={data}
) - The
renderItem
is wrapped in aTouchableOpacity
- By pressing on the
renderItem
an ID gets stored in the hook - After invoking the delete function the original data gets filtered (new array) and stored in the hook data
- The
FlatList
re-renders and removes the item - 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