I've made a selection object that keeps track of which element of the list is
selected (selection is made using presses). Elements are in the list but the
top list can be rerendered during the lifetime of the app (so can the
elements). If LayoutAnimation.easeInEaseOut() is used in the way below,
interaction with list gets glitchy.
Selecting after the setTimeout event (when the top view App component
is rerendered) will result in duplicate child views, or even removals of some
child views.

import React, { Component } from 'react';
import {
AppRegistry,
StyleSheet,
Text,
View,
TouchableOpacity,
LayoutAnimation,
} from 'react-native';
class Selection {
id: ?string = null;
listeners: Map<string, (selected: boolean) => void> = new Map();
addListener = (id: string, f: (selected: boolean) => void) => {
this.listeners.set(id, f);
}
removeListener = (id: string) => {
this.listeners.delete(id);
}
setSelected = (id: string) => {
if (this.id && this.id !== id) {
const func = this.listeners.get(this.id) || function a() {};
func(false);
}
this.id = id;
const f = this.listeners.get(this.id) || function a() {};
f(true);
}
}
export default class App extends Component {
selection = new Selection();
state = { orders: [
{id: 'one',
status: 'onestatus',
},
{id: 'two',
status: 'twostatus',
},
{id: 'three',
status: 'threestatus',
},
{id: 'four',
status: 'fourstatus',
},
{id: 'five',
status: 'fivestatus',
},
{id: 'six',
status: 'sixstatus',
},
]};
componentWillUpdate = () => {
LayoutAnimation.easeInEaseOut();
}
modifyState = () => {
const copyArr = this.state.orders.slice();
copyArr[3].status = Math.random();
this.setState({ orders: copyArr });
}
render() {
setTimeout(this.modifyState, 5000);
return (
<View style={styles.container}>
{this.state.orders.map((v) => {
return (<WrapperComponent
key={v.id}
info={v.id}
status={v.status}
selection={this.selection}
/>);
})}
</View>)
}
}
class WrapperComponent extends React.Component {
state: {selected: boolean} = {selected: false};
shouldComponentUpdate = (nextProps, nextState) => {
return this.props.status !== nextProps.status ||
this.state.selected !== nextState.selected;
}
constructor(props: Props) {
super(props);
const selection = props.selection;
const info = props.info;
selection && selection.addListener(info, this.markAsSelected);
}
markAsSelected = (status: boolean) => {
this.setState({ selected: status });
}
componentWillUnmount = () => {
const selection = this.props.selection;
const info = this.props.info;
selection && selection.removeListener(info);
}
onPress = () => {
const selection = this.props.selection;
selection && selection.setSelected(this.props.info);
}
render() {
const selected = this.state.selected;
return (
<TouchableOpacity key={`${this.props.info}${selected}`} style={{ borderWidth: 5, borderColor: selected ? 'red' : 'black' }} onPress={this.onPress}>
<Text style={styles.instructions}>
{this.props.info}
</Text>
<Text style={styles.instructions}>
{this.props.status}
</Text>
<Text style={styles.instructions}>
{selected}
</Text>
</TouchableOpacity>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
backgroundColor: '#F5FCFF',
},
instructions: {
textAlign: 'center',
color: '#333333',
marginBottom: 5,
},
});
AppRegistry.registerComponent('App', () => App);
Avoiding this bug can be accomplished by removing the easeInEaseOut call, or
by removing the key property in the TouchableOpacity.
Can reproduce on iOS react-native 0.42.3 with emulator and real device. The pressable element does not have to be TouchableOpacity - works with a variety of components.
The reason why the bug is happening is because the order of
insertReactSubview and removeReactSubview is incorrect. If easeInEaseOut
is removed then we can see that first removeReactSubview is called and then
insertReactSubview is called. If easeInEaseOut is present then
insertReactSubview will be called first twice and then removeReactSubview
twice (two elements changed, unselected list element turned into selected
one and vice versa). The problem is that removeReactSubview might have wrong
subview in arguments (attached .gif illustrates the duplication - one component on top the other - and removal - whole subview disappears).
Seems to be related to #11828 #11962 .
This is probably related to bugs with layout delete animations. The issue is pretty complex and sadly I don't have time to figure out a proper fix for it at the moment. One way you can work around the issue is create the layout animation without the delete one with:
LayoutAnimation.configureNext({
duration: 300,
create: {
type: LayoutAnimation.Types.easeInEaseOut,
property: LayoutAnimation.Properties.opacity,
},
update: { type: LayoutAnimation.Types.easeInEaseOut },
});
If someone would like to put up a PR to remove delete animation from the default preset that could be a good temporary solution until we can fix the underlying issue.
This also seems related to #13141
I have the same problem ... No new solution ?
The issue should be fixed currently on master for iOS. Android still has some issues.
Which release is the fix planned to be included in?
Any update here ? Is it fix on Android ?
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Maybe the issue has been fixed in a recent release, or perhaps it is not affecting a lot of people. If you think this issue should definitely remain open, please let us know why. Thank you for your contributions.
Is there still no fix for Android for this bug?
Most helpful comment
This is probably related to bugs with layout delete animations. The issue is pretty complex and sadly I don't have time to figure out a proper fix for it at the moment. One way you can work around the issue is create the layout animation without the delete one with:
If someone would like to put up a PR to remove delete animation from the default preset that could be a good temporary solution until we can fix the underlying issue.