1

I'm running into an odd problem with react native using react native gestures and reanimated 2.0. I am currently trying to make a simple to-do list that implements a 'swipe to delete' on tasks themselves. Everything works well up until I delete a task by filtering the task from the state. For some reason this causes the animation of one task to be passed onto the next that now occupies the index of the previous one that was deleted. For testing purposes, I am reducing the height to 20 and the translateX of the task less than it should be.

bug occuring

TestScreen.js

import { ScrollView, StyleSheet, Text, View } from "react-native";
import React, { useState } from "react";
import TestTask from "../components/TestTask";
import { useSharedValue } from "react-native-reanimated";

const names = [
    { id: 0, name: "first ting" },
    { id: 1, name: "second ting" },
    { id: 2, name: "third ting" },
    { id: 3, name: "fourth ting" },
    { id: 4, name: "fifth ting" },
];

const TestScreen = () => {
    const [tasks, setTasks] = useState(
        names.map((task) => {
            return {
                ...task,
            };
        })
    );

    const deleteTask = (id) => {
        setTasks((tasks) => tasks.filter((task) => task.id !== id));
    };

    return (
        <View>
            <ScrollView>
                {tasks.map((task, index) => (
                    <TestTask key={index} task={task} deleteTask={deleteTask} />
                ))}
            </ScrollView>
        </View>
    );
};

export default TestScreen;

const styles = StyleSheet.create({});

TestTask.js

import { Dimensions, StyleSheet, Text, View } from "react-native";
import React, { useEffect } from "react";
import Animated, {
    runOnJS,
    useAnimatedStyle,
    useSharedValue,
    withTiming,
} from "react-native-reanimated";
import { Gesture, GestureDetector } from "react-native-gesture-handler";

const { width: screenWidth } = Dimensions.get("window");
const deleteX = -screenWidth * 0.3;

const TestTask = ({ task, deleteTask }) => {
    const height = useSharedValue(50);
    const translateX = useSharedValue(0);

    const animStyles = useAnimatedStyle(() => {
        return {
            height: height.value,
            transform: [{ translateX: translateX.value }],
        };
    });

    useEffect(() => {
        return () => {
            height.value = 50;
            translateX.value = 0;
        };
    }, []);

    const pan = Gesture.Pan()
        .onUpdate((e) => {
            translateX.value = e.translationX;
        })
        .onEnd(() => {
            if (translateX.value < deleteX) {
                translateX.value = withTiming(-100);
                height.value = withTiming(20, undefined, (finished) => {
                    if (finished) {
                        runOnJS(deleteTask)(task.id);
                    }
                });
            } else {
                translateX.value = withTiming(0);
            }
        });

    return (
        <GestureDetector gesture={pan}>
            <Animated.View style={[styles.container, animStyles]}>
                <Text>{task.name}</Text>
            </Animated.View>
        </GestureDetector>
    );
};

export default TestTask;

const styles = StyleSheet.create({
    container: {
        width: "100%",
        backgroundColor: "red",
        borderColor: "black",
        borderWidth: 1,
    },
});
tomikode
  • 11
  • 3

2 Answers2

0

I did find a solution which is to place the shared animation values within the task itself which is stored in state, although this feels like bad programming practice. Then you can manipulate these values within the TestTask component.

like this:

const [tasks, setTasks] = useState(
    names.map((task) => {
        return {
            ...task,
            height: useSharedValue(50),
            translateX: useSharedValue(0),
        };
    })
);

EDIT I seem to have found a loophole, by copying the state array and storing it in a variable, then setting the state to an empty array then setting state to the filtered array (filtering through the copy). For some reason this works.

const deleteTask = (id) => {
    const copy = [...tasks];
    setTasks([]);
    setTasks(copy.filter((task) => task.id !== id));
};
tomikode
  • 11
  • 3
  • Actually this appears to be causing a flash in the rest of the tasks when the delete occurs, so its not a very optimal solution – tomikode Oct 18 '22 at 22:17
  • Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Oct 23 '22 at 08:20
0

The issue is with the indexing. Your key has to be unique for each item in the list, it does not appear that you are assigning a unique id to each task (although one exists with each task).

I would change the ScrollView component of your TestScreen.js to

return (
    <View>
        <ScrollView>
            {tasks.map((task, id) => (
                <TestTask key={task.id} task={task} deleteTask={deleteTask} />
            ))}
        </ScrollView>
    </View>
);

Let me know if this works. It worked for me, but I am using a FlatList KeyExtractor instead of a scroll view.

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map

SwissCodeMen
  • 4,222
  • 8
  • 24
  • 34
apalm
  • 1
  • 2