LayoutAnimation easeInEaseOut causes children of top view to disappear or duplicate
See original GitHub issueDescription
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.
Reproduction Steps and Sample Code
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);
Solution
Avoiding this bug can be accomplished by removing the easeInEaseOut
call, or
by removing the key
property in the TouchableOpacity
.
Additional Information
- React Native version: 0.43.0-rc.4
- Platform: iOS
- Development Operating System: MacOS
- Dev tools: Xcode
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).
Issue Analytics
- State:
- Created 6 years ago
- Reactions:2
- Comments:9 (4 by maintainers)
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.
The issue should be fixed currently on master for iOS. Android still has some issues.