question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

LayoutAnimation easeInEaseOut causes children of top view to disappear or duplicate

See original GitHub issue

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

Issue Analytics

  • State:closed
  • Created 6 years ago
  • Reactions:2
  • Comments:9 (4 by maintainers)

github_iconTop GitHub Comments

9reactions
janicduplessiscommented, Mar 31, 2017

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.

1reaction
janicduplessiscommented, Jul 14, 2017

The issue should be fixed currently on master for iOS. Android still has some issues.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Trying to remove a view index above child count error
In my case I was using LayoutAnimation for my ScrollView. Inside it a map of Items. When an Item is removed from the...
Read more >
LayoutAnimation - React Native
Automatically animates views to their new positions when the next layout happens.
Read more >
Adding animations to your React Native app - Part 2
Animate the header when the user scrolls on a list. Customize the page transition animation that's set by React Navigation. Use LayoutAnimation ......
Read more >
The magic of Layout Animations in Reanimated (React Native)
In this tutorial we'll learn how to handle the new feature of Reanimated 2: Layout ... 12K views 10 months ago Animate with...
Read more >
Messing around with React Native's Layout Animation - Medium
Use the LayoutAnimation API to essentially configure how changes in the layout are to be handled. Now here is my thought process, the...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found