Error when dragging items: TypeError: Cannot assign to read-only property 'validated'
Describe the bug So I have 2 issues with react-native-draggable-flatlist, one is that if I use <></> in my ListHeaderComponent (or ListFooterComponenet), I am unable to drag images without getting the error message: ERROR TypeError: Cannot assign to read-only property 'validated'. But changing both to () => solves the issue but brings up another issue that I can't type more than one letter at a time in TextInput, though there are no error messages. I believe the second issue is due to rerendering every time the state of something inside draggable flatlist changes. Interestingly, when I try to change the state of other things in the draggable flatlist (orientation or bio) or add and remove images, I am able to now rearrange the images as per normal. In my code you will see many ways I've tried to fix this bug, all of which unsuccessful.
Platform & Dependencies
package.json:
{ "name": "test", "version": "1.0.0", "main": "node_modules/expo/AppEntry.js", "scripts": { "start": "expo start", "android": "expo start --android", "ios": "expo start --ios", "web": "expo start --web" }, "dependencies": { "@firebase/firestore": "^4.4.0", "@react-native-async-storage/async-storage": "1.18.2", "@react-native-community/datetimepicker": "7.2.0", "@react-navigation/bottom-tabs": "^6.5.11", "@react-navigation/drawer": "^6.6.6", "@react-navigation/native": "^6.1.9", "@react-navigation/native-stack": "^6.9.17", "@react-navigation/stack": "^6.3.20", "@reduxjs/toolkit": "^1.9.7", "date-fns": "^2.30.0", "dotenv": "^16.3.1", "expo": "^49.0.21", "expo-av": "~13.4.1", "expo-constants": "~14.4.2", "expo-dev-client": "~2.4.12", "expo-font": "~11.4.0", "expo-image-manipulator": "~11.3.0", "expo-image-picker": "~14.3.2", "expo-router": "^2.0.4", "expo-status-bar": "~1.6.0", "firebase": "^10.6.0", "react": "18.2.0", "react-native": "0.72.6", "react-native-dotenv": "^3.4.9", "react-native-draggable-flatlist": "^4.0.1", "react-native-elements": "^3.4.3", "react-native-fast-image": "^8.6.3", "react-native-gesture-handler": "~2.12.0", "react-native-gifted-chat": "^2.4.0", "react-native-google-places-autocomplete": "^2.5.6", "react-native-maps": "1.7.1", "react-native-reanimated": "~3.3.0", "react-native-select-dropdown": "^3.4.0", "react-native-svg": "13.9.0", "react-native-swiper": "^1.6.0", "react-native-vector-icons": "^10.0.2", "react-redux": "^8.1.3", "redux": "^4.2.1", "redux-thunk": "^2.4.2", "typescript": "^5.1.3", "yarn": "^1.22.19" }, "devDependencies": { "@babel/core": "^7.20.0" }, "android": { "package": "com.test.test" }, "private": true }
Screen that is causing the issues: `import React, { useEffect, useState, useCallback, useRef } from 'react'; import { View, ScrollView, SafeAreaView, StyleSheet, Text, TouchableOpacity, Alert, TextInput, Image, Button, Dimensions, BackHandler, ActivityIndicator } from 'react-native'; import { useFocusEffect, CommonActions } from '@react-navigation/native'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { useDispatch } from 'react-redux'; import { getDoc, updateDoc, doc, setDoc, addDoc, collection, onSnapshot, arrayUnion } from 'firebase/firestore'; import { db, storage } from '../firebase/firebase'; import { getAuth } from 'firebase/auth'; import { uploadBytesResumable, ref, getDownloadURL, deleteObject } from 'firebase/storage'; import DraggableFlatList from 'react-native-draggable-flatlist'; import * as ImagePicker from 'expo-image-picker'; import SelectDropdown from 'react-native-select-dropdown'; import DateTimePicker from '@react-native-community/datetimepicker';
import { setHasUnsavedChangesExport } from '../redux/actions'; import OptionButton from '../components/touchableHighlight/touchableHightlight'; import { COLORS, SIZES, FONT } from '../constants'; import { useAnimatedStyle } from 'react-native-reanimated';
export default function EditProfileScreen({ navigation }) {
// All data
const [userData, setUserData] = useState(null);
// Error Fixing State
const [discardChangesKey, setDiscardChangesKey] = useState(0);
const [listKey, setListKey] = useState(Math.random().toString());
// Authentication
const auth = getAuth();
const userId = auth.currentUser.uid;
// Screen
const { width } = Dimensions.get('window');
// Orientation
const [orientation, setOrientation] = useState(null);
const [startOrientation, setStartOrientation] = useState(null);
const [orientationError, setOrientationError] = useState('');
const defaultOrientation = { male: false, female: false, nonBinary: false };
const actualOrientation = orientation || defaultOrientation;
// Images
const [image, setImage] = useState([]);
const [startImage, setStartImage] = useState([]);
const [removedImage, setRemovedImage] = useState([]);
const [progress, setProgress] = useState(0);
const [refreshKey, setRefreshKey] = useState(0);
// Bio
const [bio, setBio] = useState('');
const [startBio, setStartBio] = useState('');
// Update
const [error, setError] = useState('');
// Changes
const [isLoading, setIsLoading] = useState(true);
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
// Changes > Redux
const dispatch = useDispatch();
// Get user's data
const getFirestoreData = () => {
const docRef = doc(db, 'profiles', userId);
const unsubscribe = onSnapshot(docRef, (docSnap) => {
if (docSnap.exists()) {
const holdData = docSnap.data();
setUserData(holdData);
setOrientation(holdData.orientation);
setStartOrientation(holdData.orientation);
setBio(holdData.bio);
setStartBio(holdData.bio);
if (holdData.imageURLs) {
const initialImages = holdData.imageURLs.map((url, index) => ({
id: Math.random().toString(),
uri: url,
order: index
}));
setImage(initialImages);
setStartImage(initialImages);
setRefreshKey(oldKey => oldKey + 1);
setDiscardChangesKey(oldKey => oldKey + 1);
setListKey(Math.random().toString());
} else {
setImage([]);
setStartImage([]);
setRefreshKey(oldKey => oldKey + 1);
setDiscardChangesKey(oldKey => oldKey + 1);
setListKey(Math.random().toString());
}
setIsLoading(false);
} else {
console.log('No such document!');
setIsLoading(false);
}
});
// Clean up the listener when the component unmounts
return () => unsubscribe();
};
useFocusEffect(
useCallback(() => {
setIsLoading(true);
getFirestoreData();
}, [])
);
// ORIENTATION
const handleOrientation = (id, isSelected) => {
setOrientation(prevState => {
const newOrientation = { ...prevState, [id]: isSelected };
if (Object.values(newOrientation).every(option => !option)) {
setOrientationError('Please select at least one orientation.');
} else {
setOrientationError('');
}
return newOrientation;
});
};
// IMAGES
const handleImage = async () => {
let result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.All,
allowsEditing: true,
aspect: [3, 4],
quality: 0.2,
});
if (!result.canceled) {
let newImage = {
id: Math.random().toString(),
uri: result.assets[0].uri,
order: image.length,
isNew: true,
};
setImage(prevImages => [...prevImages, newImage]);
}
};
const uploadImage = async (uri, order, id) => {
const response = await fetch(uri);
const blob = await response.blob();
const storageRef = ref(storage, `profile_pictures/${userId}/${Date.now()}`);
const uploadTask = uploadBytesResumable(storageRef, blob);
return new Promise((resolve, reject) => {
uploadTask.on(
"state_changed",
(snapshot) => {
const progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
console.log(`Upload is ${progress}% done`);
setProgress(progress.toFixed());
},
(error) => {
console.log(error);
reject(error);
},
() => {
getDownloadURL(uploadTask.snapshot.ref).then((downloadURL) => {
console.log(`File available at: ${downloadURL}`);
resolve({ url: downloadURL, id: id });
});
}
);
});
};
const renderItem = ({ item, index, drag, isActive }) => {
return (
<GestureHandlerRootView>
<View
style={{
height: 200,
backgroundColor: isActive ? 'transparent' : item.backgroundColor,
alignItems: 'center',
justifyContent: 'center',
}}
>
<View style={{ marginTop: 50 }}>
<TouchableOpacity onLongPress={drag}>
<Image key={index} source={{ uri: item.uri }} style={{ width: 150, height: 200 }} />
</TouchableOpacity>
</View>
</View>
<View style={{ flex: 1, marginTop: 35, alignItems: 'center', justifyContent: 'center' }}>
<TouchableOpacity onPress={() => removeImage(item.id)} style={{ borderWidth: 1 }}>
<Text>Remove</Text>
</TouchableOpacity>
</View>
</GestureHandlerRootView>
);
};
const removeImage = (id) => {
const imgIndex = image.findIndex((img) => img.id === id);
if (imgIndex !== -1) {
const { uri, isNew } = image[imgIndex];
if (!isNew) {
setRemovedImage((oldArray) => [...oldArray, uri]);
}
setImage((prevImages) => prevImages.filter((img) => img.id !== id));
setRefreshKey((oldKey) => oldKey + 1);
}
};
// SUBMIT
const [isSubmitting, setIsSubmitting] = useState(false);
const handleSubmit = async () => {
// if (!hasUnsavedChanges) {
// navigation.navigate('App');
// return;
// }
setIsSubmitting(true);
try {
const userDocRef = doc(db, 'profiles', userId);
const sortedImages = [...image].sort((a, b) => a.order - b.order);
const imageURLs = [];
for (let img of sortedImages) {
if (img.isNew) {
const uploadResult = await uploadImage(img.uri, img.order, img.id);
imageURLs.push(uploadResult.url);
} else {
imageURLs.push(img.uri);
}
}
let successfullyRemovedImages = [];
for (let url of removedImage) {
try {
const deleteRef = ref(storage, url);
await deleteObject(deleteRef);
successfullyRemovedImages.push(url);
} catch (error) {
console.error("Error deleting image: ", error);
}
};
setRemovedImage(prevState => prevState.filter(url => !successfullyRemovedImages.includes(url)));
await updateDoc(userDocRef, {
orientation: orientation,
bio: bio,
imageURLs: imageURLs,
});
setHasUnsavedChanges(false);
console.log("edit profile screen changed hasUnsavedChanges to false")
navigation.navigate('App');
} catch (e) {
console.error("Error submitting: ", e);
setError(e.message);
}
setIsSubmitting(false);
};
// CHANGES
useEffect(() => {
if (!isLoading) {
if (
orientation == startOrientation &&
bio == startBio &&
image == startImage
) {
setHasUnsavedChanges(false);
dispatch(setHasUnsavedChangesExport(false));
console.log("orientation no change: ", orientation)
console.log("startOrientation no change: ", startOrientation)
console.log("edit profile screen changed hasUnsavedChanges to false")
} else {
setHasUnsavedChanges(true);
dispatch(setHasUnsavedChangesExport(true));
console.log("orientation changed: ", orientation)
console.log("startOrientation changed: ", startOrientation)
console.log("edit profile screen changed hasUnsavedChanges to true")
}
}
}, [orientation, image, isLoading, bio]);
// Hardware back button
useFocusEffect(
useCallback(() => {
const backAction = () => {
if (hasUnsavedChanges) {
Alert.alert("Discard changes?", "You have unsaved changes. Are you sure you want to discard them?", [
{ text: "Don't leave", style: 'cancel', onPress: () => { } },
{
text: 'Discard',
style: 'destructive',
onPress: () => {
navigation.dispatch(
CommonActions.reset({
index: 0,
routes: [{ name: 'App' }],
})
);
},
},
]);
return true;
}
};
const backHandler = BackHandler.addEventListener("hardwareBackPress", backAction);
return () => backHandler.remove();
}, [hasUnsavedChanges, startOrientation, startImage, navigation])
);
if (isLoading || isSubmitting) {
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<ActivityIndicator size="large" />
</View>
);
}
return (
<GestureHandlerRootView style={styles.container}>
<SafeAreaView>
{!isLoading && (
<DraggableFlatList
key={[discardChangesKey, listKey]}
style={{ flex: 1, width: width }}
showsVerticalScrollIndicator={false}
data={image}
renderItem={renderItem}
keyExtractor={(item, index) => `draggable-item-${index}`}
onDragEnd={({ data }) => {
const newData = [...data].map((item, index) => ({
...item,
order: index,
}));
setImage(newData);
}}
extraData={refreshKey}
ListHeaderComponent={
<>
<View style={styles.container}>
{/* Orientation */}
<View>
{!!orientationError && <Text style={{ color: '#cf0202' }}>{orientationError}</Text>}
</View>
<View>
<>
<OptionButton id="male" text="Male" onPress={handleOrientation} selected={actualOrientation.male} />
<OptionButton id="female" text="Female" onPress={handleOrientation} selected={actualOrientation.female} />
<OptionButton id="nonBinary" text="Non-Binary" onPress={handleOrientation} selected={actualOrientation.nonBinary} />
</>
</View>
{/* Image */}
<View>
<TouchableOpacity onPress={handleImage}>
<Text style={styles.textStyle2}>Upload Image</Text>
</TouchableOpacity>
</View>
</View>
</>
}
ListFooterComponent={
<>
<View style={{ alignItems: 'center', justifyContent: 'center', marginTop: 50 }}>
{/* Bio */}
<View style={{ paddingBottom: 20 }}>
<Text>Bio:</Text>
<TextInput
autoFocus={false}
value={bio}
onChangeText={setBio}
maxLength={100}
multiline={true}
placeholder="Write about yourself..."
style={{
backgroundColor: "#f0f0f0",
paddingVertical: 4,
paddingHorizontal: 10,
width: 205.5,
}}
/>
</View>
{!!error && <Text style={{ color: '#cf0202' }}>{error}</Text>}
<TouchableOpacity activeOpacity={0.69} onPress={handleSubmit} style={styles.btnContainer}>
<View>
<Text style={styles.textStyle}>Submit</Text>
</View>
</TouchableOpacity>
</View>
</>
}
/>
)}
</SafeAreaView>
</GestureHandlerRootView>
);
}
const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: 'white', alignItems: 'center', justifyContent: 'center', }, btnContainer: { width: 200, height: 60, backgroundColor: COLORS.themeColor, borderRadius: SIZES.large / 1.25, borderWidth: 1.5, borderColor: COLORS.white, justifyContent: "center", alignItems: "center", }, textStyle: { fontFamily: FONT.medium, fontSize: SIZES.smallmedium, color: COLORS.white, }, textStyle2: { fontFamily: FONT.medium, fontSize: SIZES.smallmedium, color: 'black', }, });`
Additional context Call Stack: ERROR TypeError: Cannot assign to read-only property 'validated'
This error is located at: in VirtualizedList (created by FlatList) in FlatList in Unknown (created by AnimatedComponent(Component)) in AnimatedComponent(Component) in Unknown (created by DraggableFlatListInner) in RCTView (created by View) in View (created by AnimatedComponent(View)) in AnimatedComponent(View) in Unknown (created by DraggableFlatListInner) in Wrap (created by AnimatedComponent(Wrap)) in AnimatedComponent(Wrap) in Unknown (created by GestureDetector) in GestureDetector (created by DraggableFlatListInner) in DraggableFlatListProvider (created by DraggableFlatListInner) in DraggableFlatListInner in RefProvider in AnimatedValueProvider in PropsProvider in DraggableFlatList (created by EditProfileScreen) in RCTView (created by View) in View (created by EditProfileScreen) in RNGestureHandlerRootView (created by GestureHandlerRootView) in GestureHandlerRootView (created by EditProfileScreen) in EditProfileScreen (created by SceneView) in StaticContainer in EnsureSingleNavigator (created by SceneView) in SceneView (created by Drawer) in RCTView (created by View) in View (created by Screen) in RCTView (created by View) in View (created by Background) in Background (created by Screen) in Screen (created by Drawer) in RNSScreen in Unknown (created by InnerScreen) in Suspender (created by Freeze) in Suspense (created by Freeze) in Freeze (created by DelayedFreeze) in DelayedFreeze (created by InnerScreen) in InnerScreen (created by Screen) in Screen (created by MaybeScreen) in MaybeScreen (created by Drawer) in RNSScreenContainer (created by ScreenContainer) in ScreenContainer (created by MaybeScreenContainer) in MaybeScreenContainer (created by Drawer) in RCTView (created by View) in View (created by Drawer) in RCTView (created by View) in View (created by AnimatedComponent(View)) in AnimatedComponent(View) in Unknown (created by Drawer) in RCTView (created by View) in View (created by AnimatedComponent(View)) in AnimatedComponent(View) in Unknown (created by PanGestureHandler) in PanGestureHandler (created by Drawer) in Drawer (created by DrawerViewBase) in DrawerViewBase (created by DrawerView) in RNGestureHandlerRootView (created by GestureHandlerRootView) in GestureHandlerRootView (created by DrawerView) in RNCSafeAreaProvider (created by SafeAreaProvider) in SafeAreaProvider (created by SafeAreaInsetsContext) in SafeAreaProviderCompat (created by DrawerView) in DrawerView (created by DrawerNavigator) in PreventRemoveProvider (created by NavigationContent) in NavigationContent in Unknown (created by DrawerNavigator) in DrawerNavigator (created by DrawerStack) in EnsureSingleNavigator in BaseNavigationContainer in ThemeProvider in NavigationContainerInner (created by DrawerStack) in DrawerStack (created by RootNavigation) in RootNavigation (created by App) in ThemeProvider (created by App) in Provider (created by App) in App (created by withDevTools(App)) in withDevTools(App) in RCTView (created by View) in View (created by AppContainer) in RCTView (created by View) in View (created by AppContainer) in AppContainer in main(RootComponent), js engine: hermes
<></> means:
ListHeaderComponent={
<>
...
</>
}
() => means:
ListHeaderComponent={() =>
...
}
did you find a solution to this? I have the same error...only started when I upgraded to Expo SDK49
Same issue also after upgrading to expo 49. I think it's related to https://github.com/software-mansion/react-native-reanimated/issues/4942.
Same issue in expo 49.
I got it to work by making the ListHeaderComponent and Footer instantiated arrow functions, to stop them from being rendered. It worked for my usecase
I got it to work by making the ListHeaderComponent and Footer instantiated arrow functions, to stop them from being rendered. It worked for my usecase
This worked for my use case as well
I got it to work by making the ListHeaderComponent and Footer instantiated arrow functions, to stop them from being rendered. It worked for my usecase
This works, but causes other problems with my use case (the header has a search bar, and when using a function like this the search text keeps getting cleared when re-rendering). Hoping for another solution.
So I came up with a workaround. Create a component for your header or footer and pass it into the ListHeaderComponent props like so
ListHeaderComponent={YourHeaderComponent}
To pass in props, you will have to wrap your DraggableFlatlist in a context and access props through useContext. So far, I haven't gotten the error and am able to fill in forms without interruption.
There is a solution in the link mentioned by @MPBogdan and the problem is caused by props being passed into the hook because react-native-reanimated freezes the object (hence the error Cannot assign read-only property) passed into those hooks and if children is in the props, that causes the error. Hopefully the author of this library can investigate the code and see if props is passed in.
The fix can be found here in PR 484 It has not been committed by the maintainer/s of this lib, but I have used it and it resolves the issue. Apply it using patch-package. All credit to NoahCardoza
I got it to work by making the ListHeaderComponent and Footer instantiated arrow functions, to stop them from being rendered. It worked for my usecase
Thanks, worked for me... can anyone explain why this works?
Before:
ListHeaderComponent={listHeaderComponent}
ListFooterComponent={listFooterComponent}
After:
ListHeaderComponent={() => listHeaderComponent}
ListFooterComponent={() => listFooterComponent}