React-native: LayoutAnimation easeInEaseOut causes children of top view to disappear or duplicate

Created on 30 Mar 2017  路  9Comments  路  Source: facebook/react-native

Description

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.
bughappens

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).

Stale

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:

    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.

All 9 comments

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?

Was this page helpful?
0 / 5 - 0 ratings