Update position of All places in react-native-sortable-listview - react-native

I am using react-native-sortable-listview in react-native for sorting same places.
constructor() {
this.state = {
makers: [
{ kolkata: 'Hawrah Birdge' },
{ Delhi: 'Lal Kila' },
{ Agra: 'Taj Mahal' },
{ Mumbai: 'India Gate' },
],
allObj: {},
order: []
};
}
componentDidMount() {
const newAllObj = this.getAllObjFromMaker(this.state.makers);
const newOrder = this.getOrderFromMaker(newAllObj);
this.setState({ allObj: newAllObj, order: newOrder });
}
getAllObjFromMaker(makers) {
const allObj = makers.reduce((result, d) => {
result[`${d.coordinate.latitude}_${d.coordinate.longitude}`] = d;
return result;
}, {});
return allObj;
}
getOrderFromMaker(allObj) {
const order = Object.keys(allObj);
return order;
}
renderOneDraggableMilestone(milestone) {
const i = this.state.makers.indexOf(milestone);
return (
<TouchableOpacity {...this.props.sortHandlers}>
<Text>{i + 1}</Text>
<Text>{milestone.address}</Text>
</TouchableOpacity>
);
}
arrangedMilestoneList(e) {
const arr = this.state.makers;
arr.splice(e.to, 0, arr.splice(e.from, 1)[0]);
const newAllObj = this.getAllObjFromMaker(arr);
const newOrder = this.getOrderFromMaker(newAllObj);
this.setState({ makers: arr, allObj: newAllObj, order: newOrder
});
}
render() {
return (
<SortableListView
data={this.state.allObj}
order={this.state.order}
activeOpacity={0.5}
onRowMoved={e => {
this.arrangedMilestoneList(e);
this.forceUpdate();
}}
renderRow={(row) => this.renderOneDraggableMilestone(row)}
/>
);
}
I want to arrange places and also their position in this.state.makers as I am doing using i in renderOneDraggableMilestone. On renderRow only draggable place are render so only their position is updated. And renderRow is last to excute so forceUpdate is also not working.
How to rerender after executing renderRow. So all position could be updated.

Ok I have find a way to re-render as follow.
<SortableListView
key={this.state.count}
data={this.state.allObj}
order={this.state.order}
activeOpacity={0.5}
onRowMoved={e => {
this.setState({ count: this.state.count + 1 });
this.props.arrangedMilestoneList(e);
console.log('onRowMoved is called');
}}
onMoveEnd={() => console.log('onMoveEnd is fired')}
renderRow={(row, s1, i) => this.renderOneDraggableMilestone(row, s1, i)}
/>
What I am doing is I added a key attribute to SortableListView and update this key on each onRowMoved action. And because of this it causes re-render.

Related

FlatList in ReactNative does not update/re-render when its data changes

Hej, I advanced my FlatList in React Native with
a) inbox/archive views
and b) with standard filter functionalities.
It's working somehow, but is not production ready!
Can someone please check this (I think well-organized) code and tell me where I do what wrong?
What is not working:
a) FlatList does not always re-render/update when the stream state, which is its data prop, changes
b) FlatList does not remove an item immediately when I archive/unarchive via swipe functionality. I have to manually change the view to see ...
c) FlatList does not directly apply the filter on state, I have to click twice to make it happen ...
//React
import { View, StyleSheet, Pressable, Animated, FlatList } from "react-native";
import { useCallback, useContext, useEffect, useState, useMemo } from "react";
//Internal
import SelectBtn from "./SelectBtn";
import SessionComponent from "./SessionComponent";
import LoadingOverlay from "../notification/LoadingOverlay";
import { SessionsContext } from "../../store/context-reducer/sessionsContext";
//External
import { Ionicons } from "#expo/vector-icons";
import { useNavigation } from "#react-navigation/native";
import { database, auth } from "../../firebase";
import { ref, onValue, remove, update } from "firebase/database";
function SessionStream() {
const navigation = useNavigation();
const sessionsCtx = useContext(SessionsContext);
const currentSessions = sessionsCtx.sessions;
const [isFetching, setIsFetching] = useState(true);
const [stream, setStream] = useState([]);
const [inbox, setInbox] = useState([]);
const [archive, setArchive] = useState([]);
const [filter, setFilter] = useState([]);
const sessionList = ["Sessions", "Archive"];
const sortList = ["ABC", "CBA", "Latest Date", "Earliest Date"];
useEffect(() => {
//Fetches all sessions from the database
async function getSessions() {
setIsFetching(true);
const uid = auth.currentUser.uid;
const sessionsRef = ref(database, "users/" + uid + "/sessions/");
try {
onValue(sessionsRef, async (snapshot) => {
const response = await snapshot.val();
if (response !== null) {
const responseObj = Object.entries(response);
const sessionsData = responseObj.map((item) => {
return {
id: item[1].id,
title: item[1].title,
laps: item[1].laps,
endTime: item[1].endTime,
note: item[1].note,
identifier: item[1].identifier,
date: item[1].date,
smed: item[1].smed,
externalRatio: item[1].externalRatio,
internalRatio: item[1].internalRatio,
untrackedRatio: item[1].untrackedRatio,
archived: item[1].archived,
};
});
sessionsCtx.setSession(sessionsData);
setIsFetching(false);
} else {
sessionsCtx.setSession([]);
setIsFetching(false);
}
});
} catch (err) {
alert(err.message);
setIsFetching(false);
}
}
getSessions();
}, []);
useEffect(() => {
//Sorts sessions into archived and unarchived
setInbox(
currentSessions.filter((session) => {
return session.archived === false || session.archived === undefined;
})
);
setArchive(
currentSessions.filter((session) => {
return session.archived === true;
})
);
}, [currentSessions, archiveHandler, unArchiveHandler, sessionsCtx, stream]);
if (isFetching) {
setTimeout(() => {
return <LoadingOverlay />;
}, 5000);
}
const onPressHandler = useCallback(
//Callback to open the session
(item) => {
navigation.navigate("Detail", {
sessionID: item.id,
});
},
[onPressHandler]
);
const rightSwipeActions = useCallback(
//Swipe actions for the session list
(item, swipeAnimatedValue) => {
return (
<View
style={{
flexDirection: "row",
width: 168,
height: 132,
}}
>
{item.archived === false ? (
<Pressable
onPress={archiveHandler.bind(this, item)}
style={({ pressed }) => pressed && styles.swipePressed}
>
<View style={styles.archive}>
<Animated.View
style={[
styles.archive,
{
transform: [
{
scale: swipeAnimatedValue.interpolate({
inputRange: [0, 1],
outputRange: [0, 1],
extrapolate: "clamp",
}),
},
],
},
]}
>
<Ionicons
name="ios-archive-outline"
size={24}
color="white"
/>
</Animated.View>
</View>
</Pressable>
) : (
<Pressable
onPress={unArchiveHandler.bind(this, item)}
style={({ pressed }) => pressed && styles.swipePressed}
>
<View style={styles.unArchive}>
<Animated.View
style={[
styles.unArchive,
{
transform: [
{
scale: swipeAnimatedValue.interpolate({
inputRange: [0, 1],
outputRange: [0, 1],
extrapolate: "clamp",
}),
},
],
},
]}
>
<Ionicons
name="md-duplicate-outline"
size={24}
color="white"
/>
</Animated.View>
</View>
</Pressable>
)}
<Pressable
onPress={deleteHandler.bind(this, item)}
style={({ pressed }) => pressed && styles.pressed}
>
<View style={styles.trash}>
<Animated.View
style={[
styles.trash,
{
transform: [
{
scale: swipeAnimatedValue.interpolate({
inputRange: [0, 1],
outputRange: [0, 1],
extrapolate: "clamp",
}),
},
],
},
]}
>
<Ionicons name="trash-outline" size={24} color="white" />
</Animated.View>
</View>
</Pressable>
</View>
);
},
[rightSwipeActions]
);
const deleteHandler = useCallback(
(item) => {
try {
sessionsCtx.deleteSession(item.id); // delete from local context
const uid = auth.currentUser.uid;
const sessionRef = ref(
database,
"users/" + uid + "/sessions/" + item.id
);
remove(sessionRef); // delete from firebase
} catch (error) {
alert(error.message);
}
},
[deleteHandler]
);
const archiveHandler = (item) => {
try {
const id = item.id;
const updatedSession = {
...item, // copy current session
archived: true,
};
const uid = auth.currentUser.uid;
const sessionRef = ref(database, "users/" + uid + "/sessions/" + id);
update(sessionRef, updatedSession);
/* sessionsCtx.updateSession(id, updatedSession); */
//update inbox state
setInbox(
currentSessions.filter((session) => {
const updatedData = session.archived === false;
return updatedData;
})
);
//update archive state
setArchive(
currentSessions.filter((session) => {
const updatedData = session.archived === true;
return updatedData;
})
);
} catch (error) {
alert(error.message);
}
};
const unArchiveHandler = (item) => {
try {
const id = item.id;
const updatedSession = {
...item, // copy current session
archived: false,
};
const uid = auth.currentUser.uid;
const sessionRef = ref(database, "users/" + uid + "/sessions/" + id);
update(sessionRef, updatedSession);
/* sessionsCtx.updateSession(id, updatedSession); */
//update unarchived session list
setArchive((preState) => {
//remove the item from archived list
preState.filter((session) => session.id !== item.id);
return [...preState];
});
} catch (error) {
alert(error.message);
}
};
const selectSessionHandler = useCallback(
(selectedItem) => {
switch (selectedItem) {
case "Sessions":
setStream(inbox);
break;
case "Archive":
setStream(archive);
break;
}
},
[selectSessionHandler, inbox, archive]
);
const selectFilterHandler = (selectedItem) => {
//filter the session list
switch (selectedItem) {
case "ABC":
// Use the Array.sort() method to sort the list alphabetically in ascending order
const sortedList = stream.sort((a, b) => {
return a.title.localeCompare(b.title);
});
setStream((preState) => {
return [...sortedList];
});
break;
case "CBA":
// Use the Array.sort() method to sort the list alphabetically in descending order
const sortedList2 = stream.sort((a, b) => {
return b.title.localeCompare(a.title);
});
setStream((preState) => {
return [...sortedList2];
});
break;
case "Latest Date":
// Use the Array.sort() method to sort the list by date in descending order
const sortedList3 = stream.sort((a, b) => {
return b.date.localeCompare(a.date);
});
setStream((preState) => {
return [...sortedList3];
});
break;
case "Earliest Date":
// Use the Array.sort() method to sort the list by date in ascending order
const sortedList4 = stream.sort((a, b) => {
return a.date.localeCompare(b.date);
});
setStream((preState) => {
return [...sortedList4];
});
break;
}
};
const renderSessionItem = useCallback(({ item }) => {
return (
<Pressable
/* style={({ pressed }) => pressed && styles.pressed} */
onPress={onPressHandler.bind(null, item)}
key={item.id}
>
<SessionComponent
key={item.id}
title={item.title}
identifier={item.identifier}
date={item.date}
rightSwipeActions={rightSwipeActions.bind(null, item)}
smed={item.smed}
endTime={item.endTime}
/>
</Pressable>
);
}, []);
return (
<View style={styles.container}>
<View style={styles.menuRow}>
<SelectBtn
data={sortList}
onSelect={(item) => selectFilterHandler(item)}
/>
<SelectBtn
data={sessionList}
onSelect={(item) => selectSessionHandler(item)}
/>
</View>
<FlatList
data={stream}
renderItem={renderSessionItem}
keyExtractor={(item) => item.id}
extraData={stream}
/>
</View>
);
}
export default SessionStream;
What I already tried:
I tried ChatGPT the whole day yesterday ... ;-)
I tried updating the global state for sessions to trigger re-renders ...
I tried updating the local state as obj or via the spread operator to ...
I tried extraData prop at FlatList
I removed useCallback to make sure it doesnt somehow block ...
can you do this when you are updating your stream state
const oldStream = stream;
const newStream = newStream; // put the value that you have updated
const returnedTarget= Object.assign(stream, newStream);
setStream(returnedTarget);
The problem might be you are mutating the copy obj
ref: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
c) in your selectFilterHandler method you are updating filter and in the same method you are trying to use updated filter value. which is not possible as useState does not update value immediately.
b) in your archiveHandler i think you are not updating the setStream state. if you are trying to run
const selectSessionHandler = useCallback(
(selectedItem) => {
switch (selectedItem) {
case "Sessions":
setStream(inbox);
break;
case "Archive":
setStream(archive);
break;
}
},
[selectSessionHandler, inbox, archive]
);
this method whenever inbox/archive changes it will not work it will be work only when you call this method and any of the value in dependency array has changed. (you can use useEffect and pass and deps arr [inbox,archive] which will run every time and can update your state.

How to fix this bug with React Native Reanimate and Gesture Handler?

So I have this basic todo:
App.jsx
const ITEMS = [
{ id: 1, text: 'Example 1' },
{ id: 2, text: 'Example 2' },
{ id: 3, text: 'Example 3' }
];
export const App = () => {
const todoRefs = useSharedValue<Record<string, any>>({});
const closeOpenTodos = (id: number) => {
'worklet';
for (const key in todoRefs.value) {
if (Number(key) !== id) {
todoRefs.value[key].closeTodo();
}
}
};
return (
<ScrollView contentContainerStyle={styles.container}>
{ITEMS.map((item, index) => (
<Item key={index} {...{ ...item, todoRefs, closeOpenTodos }} />
))}
</ScrollView>
);
};
Item.jsx (i.e. todo)
const { width: SCREEN_WIDTH } = Dimensions.get('window');
const BUTTON_WIDTH = 100;
const CONTAINER_HEIGHT = 80;
const TRANSLATE_X_THRESHOLD = -SCREEN_WIDTH * 0.7;
type ItemProps = {
id: number;
text: string;
todoRefs: Record<string, any>;
closeOpenTodos: (id: number) => void;
};
type ItemContext = {
translateX: number;
};
export const Item = ({ id, text, todoRefs, closeOpenTodos }: ItemProps) => {
const translateX = useSharedValue(0);
const containerHeight = useSharedValue(CONTAINER_HEIGHT);
const dismissItem = () => {
'worklet';
containerHeight.value = withTiming(0, { duration: 100 });
translateX.value = -SCREEN_WIDTH;
};
todoRefs.value = {
...todoRefs.value,
[id]: {
closeTodo: () => {
'worklet';
translateX.value = withTiming(0);
}
}
};
const panGestureEvent = useAnimatedGestureHandler<
PanGestureHandlerGestureEvent,
ItemContext
>({
onStart: (_event, context) => {
closeOpenTodos(id);
context.translateX = translateX.value;
},
onActive: (event, context) => {
// Prevent swiping to the right
if (event.translationX > 0) {
translateX.value = 0;
return;
}
translateX.value = event.translationX + context.translateX;
},
onEnd: (event, context) => {
// If swiping to the right, close item
if (event.translationX > 0) {
translateX.value = 0;
return;
}
if (event.translationX + context.translateX < TRANSLATE_X_THRESHOLD) {
dismissItem();
return;
}
translateX.value = withSpring(-BUTTON_WIDTH);
}
});
const animatedSliderStyle = useAnimatedStyle(() => {
return {
transform: [{ translateX: translateX.value }]
};
}, []);
const animatedContainerStyle = useAnimatedStyle(() => {
return {
height: containerHeight.value
};
}, []);
return (
<Animated.View style={[styles.container, animatedContainerStyle]}>
<Pressable style={[styles.button]} onPress={() => dismissItem()}>
<Text style={styles.buttonText}>Delete</Text>
</Pressable>
<PanGestureHandler onGestureEvent={panGestureEvent}>
<Animated.View style={[styles.slider, animatedSliderStyle]}>
<Text style={styles.sliderText}>{text}</Text>
</Animated.View>
</PanGestureHandler>
</Animated.View>
);
};
Basically when a todo is swiped open, it reveals a delete button. I want it so that if another todo it swiped open, all others close.
So I'm passing down a todoRefs (which technically aren't "refs") and closeOpenTodos.
All works fine, except this approach has introduced a strange bug.
When I go to delete items, the last one keeps re-appearing.
Is there a better way to do this?

How I can resolve this : Warning: Encountered two children with the same key, `%s`

I am new to react-native and this is not me who program this app.
Could someone help me to fix this error, I think its the flatlist who cause this because it happen only I load the page or search something on the list. I know there is a lot a question about this error but I don't find a solution for me.
Warning: Encountered two children with the same key,%s. Keys should be unique so that components maintain their identity across updates.
ContactScreen.js
import React from 'react';
import { Button, View, FlatList, Alert, StyleSheet, KeyboardAvoidingView } from 'react-native';
import { ListItem, SearchBar } from 'react-native-elements';
import Ionicons from 'react-native-vector-icons/Ionicons';
import { Contacts } from 'expo';
import * as Api from '../rest/api';
import theme from '../styles/theme.style';
import { Contact, ContactType } from '../models/Contact';
class ContactsScreen extends React.Component {
static navigationOptions = ({ navigation }) => {
return {
headerTitle: "Contacts",
headerRight: (
<Button
onPress={() => navigation.popToTop()}
title="Déconnexion"
/>
),
}
};
constructor(props) {
super(props);
this.state = {
contacts: [],
search: '',
isFetching: false,
display_contacts: []
}
}
async componentDidMount() {
this.getContactsAsync();
}
async getContactsAsync() {
const permission = await Expo.Permissions.askAsync(Expo.Permissions.CONTACTS);
if (permission.status !== 'granted') { return; }
const contacts = await Contacts.getContactsAsync({
fields: [
Contacts.PHONE_NUMBERS,
Contacts.EMAILS,
Contacts.IMAGE
],
pageSize: 100,
pageOffset: 0,
});
const listContacts = [];
if (contacts.total > 0) {
for(var i in contacts.data) {
let contact = contacts.data[i];
let id = contact.id;
let first_name = contact.firstName;
let middle_name = contact.middleName;
let last_name = contact.lastName;
let email = "";
if ("emails" in contact && contact.emails.length > 0) {
email = contact.emails[0].email;
}
let phone = "";
if ("phoneNumbers" in contact && contact.phoneNumbers.length > 0) {
phone = contact.phoneNumbers[0].number;
}
listContacts.push(new Contact(id, first_name, middle_name, last_name, email, phone, ContactType.UP));
}
}
const soemanContacts = await Api.getContacts();
if (soemanContacts.length > 0) {
for(var i in soemanContacts) {
let contact = soemanContacts[i];
let id = contact.contact_id.toString();
let first_name = contact.contact_first_name
let last_name = contact.contact_last_name;
let email = contact.contact_email;
let phone = contact.contact_phone.toString();
listContacts.push(new Contact(id, first_name, "", last_name, email, phone, ContactType.DOWN));
}
}
listContacts.sort((a, b) => a.name.localeCompare(b.name));
this.setState({contacts: listContacts});
this.setState({ isFetching: false });
this.updateSearch(null);
}
async addContactAsync(c) {
const contact = {
[Contacts.Fields.FirstName]: c.firstName,
[Contacts.Fields.LastName]: c.lastName,
[Contacts.Fields.phoneNumbers]: [
{
'number': c.phone
},
],
[Contacts.Fields.Emails]: [
{
'email': c.email
}
]
}
const contactId = await Contacts.addContactAsync(contact);
}
onRefresh() {
this.setState({ isFetching: true }, function() { this.getContactsAsync() });
}
updateSearch = search => {
this.setState({ search });
if(!search) {
this.setState({display_contacts: this.state.contacts});
}
else {
const res = this.state.contacts.filter(contact => contact.name.toLowerCase().includes(search.toLowerCase()));
console.log(res);
this.setState({display_contacts: res});
console.log("contact display "+ this.state.display_contacts);
}
};
toggleContact(contact) {
switch(contact.type) {
case ContactType.SYNC:
break;
case ContactType.DOWN:
this.addContactAsync(contact);
break;
case ContactType.UP:
Api.addContact(contact);
break;
}
/*Alert.alert(
'Synchronisé',
contact.name + 'est déjà synchronisé'
);*/
}
renderSeparator = () => (
<View style={{ height: 0.5, backgroundColor: 'grey', marginLeft: 0 }} />
)
render() {
return (
<View style={{ flex: 1 }}>
<KeyboardAvoidingView style={{ justifyContent: 'flex-end' }} behavior="padding" enabled>
<SearchBar
platform="default"
lightTheme={true}
containerStyle={styles.searchBar}
inputStyle={styles.textInput}
placeholder="Type Here..."
onChangeText={this.updateSearch}
value={this.state.search}
clearIcon
/>
<FlatList
data={this.state.display_contacts}
onRefresh={() => this.onRefresh()}
refreshing={this.state.isFetching}
renderItem={this.renderItem}
keyExtractor={contact => contact.id}
ItemSeparatorComponent={this.renderSeparator}
ListEmptyComponent={this.renderEmptyContainer()}
/>
</KeyboardAvoidingView>
</View>
);
}
renderItem = (item) => {
const contact = item.item;
let icon_name = '';
let icon_color = 'black';
switch(contact.type) {
case ContactType.SYNC:
icon_name = 'ios-done-all';
icon_color = 'green';
break;
case ContactType.DOWN:
icon_name = 'ios-arrow-down';
break;
case ContactType.UP:
icon_name = 'ios-arrow-up';
break;
}
return (
<ListItem
onPress={ () => this.toggleContact(contact) }
roundAvatar
title={contact.name}
subtitle={contact.phone}
//avatar={{ uri: item.avatar }}
containerStyle={{ borderBottomWidth: 0 }}
rightIcon={<Ionicons name={icon_name} size={20} color={icon_color}/>}
/>
);
}
renderEmptyContainer() {
return (
<View>
</View>
)
}
}
const styles = StyleSheet.create({
searchBar: {
backgroundColor: theme.PRIMARY_COLOR
},
textInput: {
backgroundColor: theme.PRIMARY_COLOR,
color: 'white'
}
});
export default ContactsScreen;
I use react-native and expo for this application.
Just do this in you flatlist
keyExtractor={(item, index) => String(index)}
I think that your some of contact.id's are same. So you can get this warning. If you set the index number of the list in FlatList, you can't show this.
keyExtractor={(contact, index) => String(index)}
Don't build keys using the index on the fly. If you want to build keys, you should do it BEFORE render if possible.
If your contacts have a guaranteed unique id, you should use that. If they do not, you should build a key before your data is in the view using a function that produces unique keys
Example code:
// Math.random should be unique because of its seeding algorithm.
// Convert it to base 36 (numbers + letters), and grab the first 9 characters
// after the decimal.
const keyGenerator = () => '_' + Math.random().toString(36).substr(2, 9)
// in component
key={contact.key}
Just do this in your Flatlist
keyExtractor={(id) => { id.toString(); }}
I got same error and I fixed in this case:
do not code in this way (using async) - this will repeat render many times per item (I don't know why)
Stub_Func = async () => {
const status = await Ask_Permission(...);
if(status) {
const result = await Get_Result(...);
this.setState({data: result});
}
}
componentDidMount() {
this.Stub_Func();
}
try something like this (using then):
Stub_Func = () => {
Ask_Permission(...).then(status=> {
if(status) {
Get_Result(...).then(result=> {
this.setState({data:result});
}).catch(err => {
throw(err);
});
}
}).catch(err => {
throw(err)
});
}
componentDidMount() {
this.Stub_Func();
}

React Native FlatList extraData not rerendering

I am not able to re-render my FlatList component. I have tried everything that I can think of. I have tried updating a boolean value for extraData with this.setState, I have passed Immutable.Map to extraData. But nothing seems to work although I know my component has updated correctly and everything is in place when component re-render, but FlatList does not re-render although I have passed changed data to extraData. I just cant figure out what I am doing wrong.
Here is my component without imports and styles:
class DetailsScreen extends Component {
constructor(props) {
super(props);
const {
data: { list, letterIndexes, alphabet },
savedList
} = this.props.navigation.state.params;
this.state = {
alphabet,
letter: "A",
letterIndexes,
letterIndexesValues: Object.values(letterIndexes),
savedList: savedList || [],
list,
updated: false
};
}
componentWillMount() {
const { list, savedList, refreshFlatList, updated } = this.state;
this.setState({
list: this.updateListActiveState(list, savedList),
updated: !updated
});
}
static navigationOptions = ({ navigation }) => ({
title: `${navigation.state.params.title}`
});
/**
* Adds active prop to each object in array.
* If they exists in savedList then we set active prop as true
*
* #param {Array<Object>} list - Names list
* #param {Array<Object>} savedList - Saved names list
* #returns {Array<Object>} list
*/
updateListActiveState(list, savedList) {
return list.map(item => {
item.active = savedList.some(s => item.name === s.name);
return item;
});
}
/**
* Updates list names state with new state
*/
onClick = savedList => {
const { list, updated } = this.state;
this.setState({
list: this.updateListActiveState(list, savedList),
updated: !updated
});
};
/**
* Renders FlatList single item
*/
renderItem = ({ item }) => {
return <NameItem key={item.id} item={item} onClick={this.onClick} />;
};
onLetterPress(letter) {
const { letterIndexes } = this.state;
this.setState({ letter });
try {
this.flatListRef.scrollToIndex({
animated: true,
index: letterIndexes[letter]
});
} catch (e) {}
}
getItemLayout = (data, index) => {
return { length: ROW_HEIGHT, offset: ROW_HEIGHT * index, index };
};
handleScroll = event => {
const { letterIndexes, letterIndexesValues } = this.state;
const y = event.nativeEvent.contentOffset.y + ROW_HEIGHT;
const index = parseInt(y / ROW_HEIGHT);
let letter = "A";
let cnt = 0;
for (const key in letterIndexes) {
if (letterIndexes.hasOwnProperty(key)) {
const startIndex = letterIndexes[key];
const endIndex =
letterIndexesValues[cnt + 1] !== undefined
? letterIndexesValues[cnt + 1]
: startIndex;
if (startIndex <= index && index <= endIndex) {
letter = key;
break;
}
}
cnt += 1;
}
if (letter !== this.state.letter) {
this.setState({ letter });
}
};
render() {
const { list, letterIndexes, alphabet, savedList, updated } = this.state;
const {
alphabetContainer,
container,
letter,
letterActive,
letterLast
} = styles;
console.log(updated);
return (
<View className={container}>
<FlatList
data={list}
keyExtractor={item => item.id}
renderItem={this.renderItem}
getItemLayout={this.getItemLayout}
initialNumToRender={20}
onScrollEndDrag={this.handleScroll}
extraData={updated}
ref={ref => {
this.flatListRef = ref;
}}
/>
<ScrollView
style={alphabetContainer}
showsHorizontalScrollIndicator={false}
>
<View>
{alphabet.map((l, i) => {
const active = l === this.state.letter ? letterActive : "";
const last = i === alphabet.length - 1 ? letterLast : "";
return (
<Text
key={i}
style={[letter, active, last]}
onPress={() => this.onLetterPress(l)}
>
{l}
</Text>
);
})}
</View>
</ScrollView>
</View>
);
}
}

React-Native SwipeableListView not correct updating

When swiping the row in the SwipeableListView I want to delete the rowitem and re-render the list.
What is now happening is that always the last item in the list is removed, not the item that is swiped.
Any ideas what is wrong?
export default class SwipeList extends Component {
constructor(props) {
super(props);
let ds = SwipeableListView.getNewDataSource();
this.favourites = []
this.state = {
ds:[],
dataSource:ds,
isLoading:true,
closeRow:false,
};
}
componentWillMount () {
store.get('KEY_FAV').then(value => {
typeof(value) === 'object'
? this.favourites = Object.keys(mockdata.favourite)
: this.favourites = JSON.parse(value)
this.setState({
dataSource: this.state.dataSource.cloneWithRowsAndSections(this.genData(this.favourites
)),
isLoading:false
})
})
}
genData = (list) => {
let dataBlob = []
for(let i = 0; i <list.length; i++) {
dataBlob.push({id:list[i], name:list[list[i]]})
}
return [dataBlob, []]
}
Till here it is okay, the SwipeableList is loaded with all RowItems.
But in the below handleSwipeAction() while setting new state for dataSource, the list will only delete the last item, not the selected.
handleSwipeAction = (rowData, SectionID, rowID) => {
AlertIOS.alert('Remove ' + rowData.name + ' \nfrom Favourites?', null,
[
{text:'Cancel', onPress: () => {this.setState({closeRow:true})}, style:'cancel'},
{text:'OK', onPress: () => {
this.favourites.slice()
this.favourites.splice(rowID, 1)
this.setState({
closeRow:true,
})
this.setState({//I THINK HERE IS THE PROBLEM
dataSource:this.state.dataSource.cloneWithRowsAndSections(this.genData(this.favourites))
})
store.set('KEY_FAV', this.favourites)
}}
])
}
onSwipe = (rowData, SectionID, rowID) => {
return (
<View style={styles.actionsContainer}>
<TouchableHighlight
onPress={() => this.handleSwipeAction(rowData, SectionID, rowID)}>
<Text style={styles.actionsItem}>Remove</Text>
</TouchableHighlight>
</View>
);
};
and the render function
render() {
if(this.state.isLoading) return null
return (
<View style={styles.container}>
<SwipeableListView
bounceFirstRowOnMount
enableEmptySections={true}
dataSource={this.state.dataSource}
maxSwipeDistance={this.props.swipeDistance}
renderRow={(item) => this.renderItem(item)}
renderQuickActions={this.onSwipe}
renderSeparator={this.renderSeperator}
doCloseRow={this.state.closeRow}
/>
</View>
);
}
when you are done slicing, I believe if you do:
let ds = SwipeableListView.getNewDataSource(); all over again, and then
this.setState({ dataSource: ds.cloneWithRowsAndSections(this.genData(this.favourites)) })
It should work. For a reason that I still don't get. Also I don't know why you do two setState() in your function. One is enough no?
So this should work:
handleSwipeAction = (rowData, SectionID, rowID) => {
AlertIOS.alert('Remove ' + rowData.name + ' \nfrom Favourites?', null,
[
{text:'Cancel', onPress: () => {this.setState({closeRow:true})}, style:'cancel'},
{text:'OK', onPress: () => {
this.favourites.slice()
this.favourites.splice(rowID, 1)
let ds = SwipeableListView.getNewDataSource(); // add this
this.setState({ dataSource: ds.cloneWithRowsAndSections(this.genData(this.favourites)), closeRow:true })
store.set('KEY_FAV', this.favourites) }} ])
}