computerjazz / react-native-draggable-flatlist

A drag-and-drop-enabled FlatList for React Native
MIT License
1.88k stars 387 forks source link

Items return to original position after drag is complete #532

Open MicahDavid opened 3 months ago

MicahDavid commented 3 months ago

Describe the bug After dragging an item to a new position, after a short period, the item returns to the original position.

Platform & Dependencies react-native-draggable-flatlist version: 4.0.1

Platform: iOS

React Native or Expo version: 0.72.7

Reanimated version: 3.6.1

React Native Gesture Handler version: 2.14.0

https://github.com/computerjazz/react-native-draggable-flatlist/assets/51126961/a6c46861-7fed-4148-a350-07cea68192d4

My first guess was the list component was being re-rendered, but it is not.
Here's the basic code:

    const renderItem = ({item,drag}) => {
        return (
            item?.locationBeachId && (
                <ForecastItem
                    drag={drag}
                    key={item.locationBeachId}
                    item={item}
                    openDetails={handleShowDetail}
                />
            )
        )
    }

    return (
        <View>
            {
                favList && (
                        <DraggableFlatList
                            data={favList}
                            renderItem={renderItem}
                            keyExtractor={item => item?.locationBeachId}
                            onDragEnd={({ data, from, to }) => {
                                if (from !== to)
                                    reorderFaves(data)
                            }}
                        />
                )
            }
        </View>
    )

Any suggestions?

zacharygameiro-ploutospay commented 3 months ago

it isn't clear. can you provide more code? does it always snap back or only some of the time?

MicahDavid commented 2 months ago

it isn't clear. can you provide more code? does it always snap back or only some of the time?

Yes, it snaps back every time. Here is the entire component. Its worth noting, that this component lives inside a tab view, in case that has any relevance.

import React, {useState} from 'react'
import { connect } from 'react-redux'
import {FlatList, StyleSheet, Text} from 'react-native'
import { View } from 'native-base'
import { useNavigation } from '@react-navigation/native'

import {widthPercentageToDP as wp, heightPercentageToDP as hp} from 'react-native-responsive-screen'

import Loading from '../../../components/Loading'
import { FavoriteWarning } from '../../../components/blanks'
import { ForecastItem } from '../../../components/items'
import { getUserUnits } from '../../../utils/localStorageUtil'

import locationActions from '../../../redux/locations/actions'
const { fetchForecastItemRequest} = locationActions

import { globalStyles } from "../../../assets/styles/globalStyles"

import DraggableFlatList, {ShadowDecorator} from 'react-native-draggable-flatlist'
import axios from "axios"
import {getEndpoint} from "../../../utils/urlHelper"
import {axiosHeader} from "../../../utils/authUtil"
import {AlertModal} from "../../../components/modals"

const FavouritesContent = (props) => {

    const navigation = useNavigation()
    const { forecastSlug, favList, fetchForecastItemRequest, isOffline, showErrorMsg } = props

    const handleShowDetail = async(locationSlug) => {
        if (!isOffline) {
            if (forecastSlug !== locationSlug) {
                let units = await getUserUnits()

                fetchForecastItemRequest({
                    slug: locationSlug,
                    units: units ? units : 1
                })
            }
            navigation.navigate('ForecastDetail')
        }
    }

    const reorderFaves = (data) => {
        let orderArray = []
        data.forEach(loc => {
            orderArray.push(loc.id)
        })

        axios.patch(getEndpoint('api/favorites/reorder'),
            {newOrder: orderArray},
            {headers: axiosHeader}
        ).then(function (response) {
            if (response.hasOwnProperty('data') && response.data.hasOwnProperty('errorMsg') && response.data.errorMsg)
                showErrorMsg(response.data.errorMsg)
        })
        .catch(function (error) {
            showErrorMsg('Error: '+error)
        })
    }

    const renderItem = ({item,drag,isActive}) => {
        return (
            item?.locationBeachId && (
                <ShadowDecorator>
                    <ForecastItem
                        drag={drag}
                        dragIsActive={isActive}
                        item={item}
                        openDetails={handleShowDetail}
                    />
                </ShadowDecorator>
            )
        )
    }

    return (
        <View style={{flexGrow:1}}>
            {
                favList && (
                    <View style={{position:'relative'}}>
                        <DraggableFlatList
                            data={favList}
                            renderItem={renderItem}
                            keyExtractor={item => item?.locationBeachId}
                            /*
                            onDragEnd={({ data, from, to }) => {
                                if (from !== to)
                                    reorderFaves(data)

                                //console.log('data',data)
                            }}
                             */
                            contentContainerStyle={{paddingBottom:40}}
                        />
                        <View style={styles.favReorderContainer}>
                            <Text style={styles.favReorderText}>Press and hold location, then drag to re-order.</Text>
                        </View>
                    </View>
                )
            }
        </View>
    )
}

const Favorites = (props) => {

    const { favList, fLoading, setShowSLocationModal } = props

    const [errorModal, setErrorModal] = useState({
        title: null,
        content: null,
        open: false
    })

    return (
        <View style={styles.container}>
            {
                fLoading === true && (
                    <Loading/>
                )
            }
            <View style={styles.mainContent}>
                {
                    (!favList || favList.length === 0) ?
                        <FavoriteWarning
                            setShowSLocationModal={setShowSLocationModal}
                        /> :
                        <FavouritesContent
                            favList={favList}
                            showErrorMsg={(message) => {
                                setErrorModal({
                                    title: 'Error',
                                    content: message,
                                    open: !!message
                                })
                            }}
                            {...props}
                        />
                }
            </View>

            <AlertModal
                data={errorModal}
                visible={errorModal.open}
                onClose={() =>
                    setErrorModal({
                        title: null,
                        content: null,
                        open: false
                    })
                }
            />
        </View>
    )
}

const styles = StyleSheet.create({
    container: {
        flex:1,
        backgroundColor: globalStyles.colors.paleBlue,
    },
    mainContent: {
        marginTop: hp(1),
        marginBottom: hp(1),
        zIndex:1
    },
    mapView: {
        position: 'absolute',
        bottom: hp(3),
        right: wp(3)
    },
    favReorderContainer: {
        position:'absolute',
        bottom:0,
        left:0,
        width:'100%',
        backgroundColor: globalStyles.colors.paleBlue,
        paddingTop:10
    },
    favReorderText: {
        fontFamily:'Roboto',
        fontSize:14,
        color:globalStyles.colors.gray500,
        textAlign:'center',
    },
    errorMsgContainer: {
        backgroundColor:globalStyles.colors.warning500,
        borderRadius:4,
        alignItems:'center',
        marginHorizontal:10,
        marginVertical:8,
        flexBasis:'auto'
    },
    errorMsgText: {
        fontFamily:'Roboto-Medium',
        color:'#fff',
        fontSize:16,
        lineHeight:28
    }
})

const mapStateToProps = state => ({
    fLoading: state.locationReducer.favLoading,
    forecastSlug: state.locationReducer.forecastSlug,
    isOffline: state.authReducer.isOffline
})

const mapDispatchToProps = {
    fetchForecastItemRequest
}

export default connect(mapStateToProps, mapDispatchToProps)(Favorites)

Here is the rendered item component:

import React, {memo} from 'react'
import { connect } from 'react-redux'
import { useWindowDimensions, Pressable, Text } from 'react-native'
import { View, Icon } from 'native-base'
import HTML from 'react-native-render-html'

import { RFValue } from "react-native-responsive-fontsize"
import { forecastItemStyles } from "../../assets/styles/ForecastDayItem"

//import { WI_ICONS } from '../../assets/images/wx-icons'
import { globalStyles } from '../../assets/styles/globalStyles'
import WindIcon from '../../assets/images/icon/wind-arrow-summary.svg'
import {windArrowStyles} from "../../assets/styles/timeforecast"
import Ionicons from 'react-native-vector-icons/Ionicons'

const ForecastItem = (props) => {

    const {item, openDetails, drag, dragIsActive} = props

    const handleOpenDetails = () => {
        openDetails(item.locationBeachSlug)
    }

    const { width } = useWindowDimensions();

    //const WICON = item?.wxicon ? WI_ICONS[Object.keys(WI_ICONS).includes(item.wxicon) ? item.wxicon : 'none'] : WI_ICONS['none']

    return (
        <Pressable
            onLongPress={drag ? drag : null}
            disabled={dragIsActive ? dragIsActive : null}
            onPress={handleOpenDetails}
            style={({ pressed }) => [forecastItemStyles.container, {borderWidth: (dragIsActive ? 1 : 0),borderColor: globalStyles.colors.gray350, backgroundColor: dragIsActive || pressed ? globalStyles.colors.blue50 : 'white'}]}
        >
                <View style={forecastItemStyles.itemLeftPart}>
                    { /*
                    <View style={{flexDirect:'row'}}>
                        <View style={{flexDirection:'row',flex:1,flexWrap: 'wrap', justifyContent:'center'}}>
                            <WICON width={RFValue(30)} height={RFValue(30)} style={{...forecastItemStyles.itemWeatherIcon,width:30}} fill="#00111C" />
                            <Text style={{...forecastItemStyles.itemTempText,paddingLeft:2, fontSize:12}}>
                                {item?.weather.atmp + "\u00B0"}
                            </Text>
                        </View>
                    </View>
                    */
                    }
                    <View style={{flexDirect:'row', justifyContent:'center', alignItems: 'center'}}>
                        <WindIcon width={14} height={18}
                                  style={{ ...windArrowStyles[item?.weather?.wind_dir ? 'dir-'+(item.weather.wind_dir).toLowerCase() : 'dir-'], marginRight: 6}}
                        />
                        <Text style={{...forecastItemStyles.itemWindText, marginTop:16}}>{item?.weather.wind_dir+' '+item?.weather.wind_spd_digit}
                            <Text style={{fontSize:9}}>{item?.weather.wind_spd_unit}</Text>
                        </Text>
                    </View>
                </View>

                <View style={forecastItemStyles.itemRightPart}>
                    <View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
                        <Text style={forecastItemStyles.itemTitle}>
                            {
                                item?.locationName && (
                                    <HTML
                                        defaultTextProps={{allowFontScaling:false}}
                                        contentWidth={width}
                                        source={{ html: `<div class="location-title">${item.locationName}</div>` }}
                                        classesStyles={{
                                            'location-title': {
                                                fontFamily: 'Roboto-Medium',
                                                color: globalStyles.colors.blue800,
                                                fontSize: 18,
                                            }
                                        }}
                                    />
                                )
                            }
                        </Text>
                        <Text style={forecastItemStyles.itemDistance}>

                        </Text>
                    </View>
                    <View style={forecastItemStyles.itemBottomBtnGroup}>
                        <View
                            style={[
                                forecastItemStyles.itemBtn,
                                item?.condAm ? item.condAm === 'choppy' ? forecastItemStyles.itemChoppyBtn : (item.condAm === 'fair' ? forecastItemStyles.itemFairBtn : (item.condAm === 'clean' ? forecastItemStyles.itemCleanBtn : {})) : {}
                            ]}
                        >
                            <Text style={forecastItemStyles.itemTimeBtnLText}>
                                AM
                            </Text>
                            <Text style={forecastItemStyles.itemTimeBtnRText}>
                                {item?.surfAm}
                            </Text>
                        </View>
                        <View
                            style={[
                                forecastItemStyles.itemBtn,
                                item?.condPm ? item.condPm === 'choppy' ? forecastItemStyles.itemChoppyBtn : (item.condPm === 'fair' ? forecastItemStyles.itemFairBtn : (item.condPm === 'clean' ? forecastItemStyles.itemCleanBtn : {})) : {}
                            ]}
                        >
                            <Text style={forecastItemStyles.itemTimeBtnLText}>
                                PM
                            </Text>
                            <Text style={forecastItemStyles.itemTimeBtnRText}>
                                {item?.surfPm}
                            </Text>
                        </View>
                        <View style={forecastItemStyles.itemDetails}>
                            <Icon
                                as={Ionicons}
                                name="chevron-forward"
                                size="lg"
                                style={forecastItemStyles.itemDetailsIcon}
                            />
                        </View>
                    </View>
                </View>
        </Pressable>
    )
}

export default memo(ForecastItem)

Thanks for any suggestions.

MicahDavid commented 2 months ago

I got rid of everything except a simple test list, and still the draggable item returns to its original position.

Here is a test component that I rendered without anything else:

import React, {useState} from 'react'
import {Text, Pressable} from 'react-native'
import { View } from 'native-base'

import DraggableFlatList from 'react-native-draggable-flatlist'

const renderItem = ({item,drag,isActive}) => {
    return (
            <Pressable
                onLongPress={drag ? drag : null}
                disabled={isActive ? isActive : null}
            >
                <View style={{height:50,borderWidth:2,borderColor:'red'}}>
                    <Text>{item.name}</Text>
                </View>
            </Pressable>
    )
}

const Favorites = () => {

    //const { favList, fLoading, setShowSLocationModal } = props

    const favList = [
        {
            id: 1,
            name: 'A'
        },
        {
            id: 2,
            name: 'B'
        },
        {
            id: 3,
            name: 'C'
        }
    ]

    return (
        <View>
            {
                favList && (
                    <View>
                        <DraggableFlatList
                            data={favList}
                            renderItem={renderItem}
                            keyExtractor={item => item?.id}
                        />
                    </View>
                )
            }
        </View>
    )
}

 export default Favorites

Not sure what else to try.

MicahDavid commented 2 months ago

I'm a colossal idiot. I somehow overlooked that I needed to set the data on the re-order function. Please accept my apology on behalf of my idiocy and for any waste of time. Hopefully if someone is as bit of an idiot as me, they will run across this thread.