Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Error when dragging items: TypeError: Cannot assign to read-only property 'validated' #519

Open
jianrontan opened this issue Dec 27, 2023 · 9 comments

Comments

@jianrontan
Copy link

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={() =>
...
}

@Svarto
Copy link
Contributor

Svarto commented Jan 2, 2024

did you find a solution to this? I have the same error...only started when I upgraded to Expo SDK49

@MPBogdan
Copy link

MPBogdan commented Jan 2, 2024

Same issue also after upgrading to expo 49. I think it's related to software-mansion/react-native-reanimated#4942.

@arashlabafian
Copy link

Same issue in expo 49.

@Svarto
Copy link
Contributor

Svarto commented Jan 5, 2024

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

@shikhin21
Copy link

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

@marcshilling
Copy link

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.

@Rc85
Copy link

Rc85 commented Mar 30, 2024

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.

@varem611
Copy link

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

@inisson
Copy link

inisson commented Jul 13, 2024

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}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

9 participants