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.
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,
},
});
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));
};