react-native-reanimated: Animated.FlatList is broken

Description

Button in FlatList is not pressable after the element is unmounted. Kinda hard to explain verbally, please take a look at the video below. Interestingly, if you used Animated.FlatList before, and swap it back to regular FlatList, the regular FlatList will be affected as well. You need to restart the bundler to resolve the problem.

Expected behavior

Pressable in next element should be pressable after the previous element is removed.

https://user-images.githubusercontent.com/15872787/145156335-3af68ea6-167e-45b7-94df-438a03cf750c.mp4

Actual behavior & steps to reproduce

As you can see in the video, after deleting one element, the Pressable in next element is not pressable.

https://user-images.githubusercontent.com/15872787/145156207-1888ad3f-1da9-4378-b3ff-652bdd65c95a.mp4

Snack or minimal code example

diff between these implementation is within the FlatList only

React Native's FlatList implementation
import React, {useState} from 'react';
import {
  Button,
  View,
  Text,
  FlatList,
  TextInput,
  SafeAreaView,
} from 'react-native';

function Participant({name, onRemove, id}) {
  return (
    <View style={[styles.participantView]}>
      <Text>{`${name};${id}`}</Text>
      <Button title="Remove" color="red" onPress={onRemove} />
    </View>
  );
}

const App = () => {
  const [inputValue, setInputValue] = useState('Reanimated');
  const [participantList, setParticipantList] = useState([]);

  const addParticipant = () => {
    setParticipantList(
      [{name: inputValue, id: Date.now().toString()}].concat(participantList),
    );
  };

  const renderParticipant = React.useCallback(({item: participant}) => {
    const removeParticipant2 = () => {
      setParticipantList(prev => {
        return prev.filter(prevPar => prevPar.id !== participant.id);
      });
    };
    return (
      <Participant
        key={participant.id + 'FLATLIST'}
        id={participant.id}
        name={participant.name}
        onRemove={removeParticipant2}
      />
    );
  }, []);

  return (
    <SafeAreaView style={{flex: 1, width: '100%'}}>
      <View style={styles.listView}>
        <Text>FlatList</Text>
        <FlatList
          data={participantList}
          style={[{width: '100%'}]}
          renderItem={renderParticipant}
        />
      </View>

      <View style={[styles.bottomRow]}>
        <View style={[styles.textInput]}>
          <Text>Add participant: </Text>
          <TextInput
            placeholder="Name"
            value={inputValue}
            onChangeText={setInputValue}
          />
        </View>

        <Button
          title="Add"
          disabled={inputValue === ''}
          onPress={addParticipant}
        />
      </View>
    </SafeAreaView>
  );
};

const styles = {
  participantView: {
    borderBottomColor: 'black',
    flex: 1,
    borderBottomWidth: 1,
    padding: 10,
    display: 'flex',
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'space-between',
    backgroundColor: '#fffbeb',
  },
  listView: {
    flex: 1,
    flexDirection: 'column',
    alignItems: 'center',
    justifyContent: 'space-between',
    width: '100%',
  },
  bottomRow: {
    width: '100%',
    display: 'flex',
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    padding: 10,
  },
  textInput: {
    display: 'flex',
    flexDirection: 'row',
    alignItems: 'center',
  },
};

export default App;

Problematic Animated.FlatList implementation
import React, {useState} from 'react';
import {Button, View, Text, TextInput, SafeAreaView} from 'react-native';
import Animated, {
  Layout,
  LightSpeedInLeft,
  LightSpeedOutLeft,
} from 'react-native-reanimated';

function Participant({name, onRemove, id}) {
  return (
    <Animated.View
      style={[styles.participantView]}
      entering={LightSpeedInLeft}
      exiting={LightSpeedOutLeft}
      layout={Layout.springify()}>
      <Text>{`${name};${id}`}</Text>
      <Button title="Remove" color="red" onPress={onRemove} />
    </Animated.View>
  );
}

const App = () => {
  const [inputValue, setInputValue] = useState('Reanimated');
  const [participantList, setParticipantList] = useState([]);

  const addParticipant = () => {
    setParticipantList(
      [{name: inputValue, id: Date.now().toString()}].concat(participantList),
    );
  };

  const renderParticipant = React.useCallback(({item: participant}) => {
    const removeParticipant2 = () => {
      setParticipantList(prev => {
        return prev.filter(prevPar => prevPar.id !== participant.id);
      });
    };
    return (
      <Participant
        key={participant.id + 'FLATLIST'}
        id={participant.id}
        name={participant.name}
        onRemove={removeParticipant2}
      />
    );
  }, []);

  return (
    <SafeAreaView style={{flex: 1, width: '100%'}}>
      <View style={styles.listView}>
        <Text>Animated.FlatList</Text>
        <Animated.FlatList
          itemLayoutAnimation={Layout.springify()}
          data={participantList}
          style={[{width: '100%'}]}
          renderItem={renderParticipant}
        />
      </View>

      <View style={[styles.bottomRow]}>
        <View style={[styles.textInput]}>
          <Text>Add participant: </Text>
          <TextInput
            placeholder="Name"
            value={inputValue}
            onChangeText={setInputValue}
          />
        </View>

        <Button
          title="Add"
          disabled={inputValue === ''}
          onPress={addParticipant}
        />
      </View>
    </SafeAreaView>
  );
};

const styles = {
  participantView: {
    borderBottomColor: 'black',
    flex: 1,
    borderBottomWidth: 1,
    padding: 10,
    display: 'flex',
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'space-between',
    backgroundColor: '#fffbeb',
  },
  listView: {
    flex: 1,
    flexDirection: 'column',
    alignItems: 'center',
    justifyContent: 'space-between',
    width: '100%',
  },
  bottomRow: {
    width: '100%',
    display: 'flex',
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    padding: 10,
  },
  textInput: {
    display: 'flex',
    flexDirection: 'row',
    alignItems: 'center',
  },
};

export default App;

Package versions

  • React Native: 0.66.3
  • React Native Reanimated: 2.3
  • NodeJS:
  • Xcode:
  • Java & Gradle:

Affected platforms

  • Android
  • iOS (not sure)
  • Web (not sure)

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Reactions: 12
  • Comments: 18 (4 by maintainers)

Most upvoted comments

This issue happens without Animated.Flatlist with a normal FlatList I narrowed it down to using Animated.View instead a normal FlatList

import * as SplashScreen from "expo-splash-screen";
import React, {useEffect, useState} from "react";
import {Button, FlatList, SafeAreaView, Text, TextInput, View} from "react-native";
import Animated from "react-native-reanimated";

function Participant({name, onRemove, id}) {
    return (
        <Animated.View
            style={[styles.participantView]}
            entering={LightSpeedInLeft}
            // exiting={LightSpeedOutLeft}
            // layout={Layout.springify()}
        >
            <Text>{`${name};${id}`}</Text>
            <Button title="Remove" color="red" onPress={onRemove} />
        </Animated.View>
    );
}

const App = () => {
    const [inputValue, setInputValue] = useState("Reanimated");
    const [participantList, setParticipantList] = useState([]);

    const addParticipant = () => {
        setParticipantList(
            [{name: inputValue, id: Date.now().toString()}].concat(
                participantList
            )
        );
    };

    const renderParticipant = React.useCallback(({item: participant}) => {
        const removeParticipant2 = () => {
            console.log("s");
            setParticipantList((prev) => {
                return prev.filter((prevPar) => prevPar.id !== participant.id);
            });
        };
        return (
            <Participant
                key={participant.id + "FLATLIST"}
                id={participant.id}
                name={participant.name}
                onRemove={removeParticipant2}
            />
        );
    }, []);

    useEffect(() => {
        SplashScreen.hideAsync();
    }, []);

    return (
        <SafeAreaView style={{flex: 1, width: "100%", backgroundColor: "red"}}>
            <View style={styles.listView}>
                <Text>Animated.FlatList</Text>
                <FlatList
                    // itemLayoutAnimation={Layout.springify()}
                    data={participantList}
                    style={[{width: "100%"}]}
                    renderItem={renderParticipant}
                />
            </View>

            <View style={[styles.bottomRow]}>
                <View style={[styles.textInput]}>
                    <Text>Add participant: </Text>
                    <TextInput
                        placeholder="Name"
                        value={inputValue}
                        onChangeText={setInputValue}
                    />
                </View>

                <Button
                    title="Add"
                    disabled={inputValue === ""}
                    onPress={addParticipant}
                />
            </View>
        </SafeAreaView>
    );
};

const styles = {
    participantView: {
        borderBottomColor: "black",
        flex: 1,
        borderBottomWidth: 1,
        padding: 10,
        display: "flex",
        flexDirection: "row",
        alignItems: "center",
        justifyContent: "space-between",
        backgroundColor: "#fffbeb",
    },
    listView: {
        flex: 1,
        flexDirection: "column",
        alignItems: "center",
        justifyContent: "space-between",
        width: "100%",
    },
    bottomRow: {
        width: "100%",
        display: "flex",
        flexDirection: "row",
        justifyContent: "space-between",
        alignItems: "center",
        padding: 10,
    },
    textInput: {
        display: "flex",
        flexDirection: "row",
        alignItems: "center",
    },
};

export default App;

So it doesn’t have anything to do with Animated.Flatlist and even layout prop.

Also tested on iOS and it works fine there.

just removing the entering makes it also work on android.

Created another issue with minimal example feel free to test : https://github.com/software-mansion/react-native-reanimated/issues/3029

This even happens when i just use an Animated.View with one of the default layoutanimations anywhere on my screen. As soon as i do that “ghost” views stick around on Android. Like the ListEmptyComponent for instance.

I’m having the same issue after updating Expo SDK 43 > 44 which also updated reanimated from v2.2.x > 2.3.x.

When the Animated.FlatList data changes, there are left over “zombie” children from the previous data set or if the listEmptyComponent was rendered, it stays “on screen” but neither can be interacted with and both show as unmounted.

This only seems to happen after I trigger any of the new API Layout methods (ie. entering, exiting) anywhere in the app, it doesn’t have to be triggered from within the aforementioned FlatList or from within the same screen. Once I removed the entering and exiting props from Animated.View the FlatList was working as intended.

And as @Elabar mentioned, you need to restart the bundler to see the changes.

@Latropos you tested it in both Platforms? Because in iOS apparently is not working. I mean, the prop itemLayoutAnimation do nothing in mi case. I’ve the last version of Reanimated and it is my flatlist:

              <Animated.FlatList
                  data={sessions}
                  style={Style.list}
                  renderItem={renderItem}
                  itemLayoutAnimation={FadeIn}
                  keyExtractor={item => item.id}
                  ItemSeparatorComponent={<View style={{height: 16}}/>}
              />

I’ve just tested it with the newest reanimated - everything seems to work correctly! 🥳 Thanks to everybody for submitting and commenting the issue.

Does anyone know if this issue still exists in reanimated 3? We’ve been blocked from using the entering/exiting/layout apis because of this issue and are wondering if investing the time to upgrade to one of the version 3 rc’s would be worth it.

I am experiencing the exact same thing as @jdlk07

Similar issue here with v2.3.0 + LayoutAnimation. After removing a flatlist entry and re-adding it (same key) after a couple renders, the touchable for that entry does not work.