Pushing a flatlist item up or down - react-native

I have a flatlist. The flatlist contains todos that I can add, edit and delete.
I have two buttons on each todo-item, one for up and one for down.
If I want to push them up 1 step (until it reaches 0) or down (until it reaches the last item) how would I do this? Help is much appreciated, this feels basic but I can't seem to find an answer that works.
Here is my code:
<FlatList
removeClippedSubviews={false}
data = {todos.sort(function(a,b){return b.date - a.date})}
keyExtractor={(item)=>item.key}
renderItem={({item})=>{
return(
<TouchableOpacity>
<View>
<TextInput
style={styles.toDoItem}
value={item.title}
onChangeText={(text)=>handleUpdateText(text, item)}
multiline={true}
maxLength={55}
blurOnSubmit={true}
/>
<TouchableOpacity onPress={()=>handleMoveUp(item.key, item)}>
<FontAwesome name="plus-circle" size={30} color={black}/>
</TouchableOpacity>
<TouchableOpacity onPress={()=>handleMoveDown(item.key, item)}>
<FontAwesome name="minus-circle" size={50} color={black}/>
</TouchableOpacity>

The FlatList's data must first be stored into a state.
const [todos, setTodos] = useState([])
Since you are sorting the initial states in your FlatList according to their dates, it might be useful to do this exactly once in the following useEffect.
React.useEffect(() => {
let temp = [...todos]
temp.sort(function(a,b){return b.date - a.date})
setTodos(temp)
}, [setTodos])
We could also do this directly in the initial state.
Use todos of your state in your FlatList.
<FlatList
data = {todos}
...
</FlatList
...
Then, implement a function that alters the state of your items as follows.
const handleMoveUp = React.useCallback(
(index) => {
if (index !== 0) {
let prevState = [...todos]
let temp = prevState[index - 1]
prevState[index - 1] = prevState[index]
prevState[index] = temp
setItems(prevState)
}
},
[setTodos, todos]
)
add this function as the onPress function of your TouchableOpacity that moves items up, that is
<TouchableOpacity onPress={()=>handleMoveUp(item.key)}>
<FontAwesome name="plus-circle" size={30} color={black}/>
</TouchableOpacity>
The process for moving down is implemented in a very similar fashion. Just use the same code as in handleMoveUp but increase the index instead of decreasing it.

Related

React native FlatList not rerendering when data prop changes to empty array

I have a FlatList with a data prop pulling from Redux
render() {
return (
<View>
<FlatList
data={this.props.arrayOfPlacesFromRedux}
renderItem={({item}) => {.......
Whenever I dispatch changes to arrayOfPlacesFromRedux(i.e. adding or removing children), the FlatList rerenders....UNLESS I remove all children from array (i.e. make length zero).When arrayOfPlacesFromRedux changes from a positive length to a length of zero, the FlatList does not rerender.....however all other types of changes to array do indeed cause FlatList to rerender
UPDATE 02/27
Below is my reducer used to update Redux arrayOfPlacesFromRedux
const reducer = (state = initialState, action) => {
switch (action.type) {
case UPDATE_PLACES_ARRAY:
return {...state, arrayOfPlaces: action.payload};
default:
return state;
}
};
In the situation noted above when FlatList does not rerender.....action.payload is an empty array
The question is missing some important piece of code.
React as well as Redux need arrays reference to change, meaning for a component to reRender on state change, the array references needs to change.
Live demo at https://snack.expo.dev/RrFFxfeWY
Here is the most interesting parts:
If you have a basic component as below:
const MyList = () => {
const [data, setData] = React.useState([
'#FF0000',
'#FF8000',
'#FFFF00',
]);
return (
<>
<Text>List poping is not working</Text>
<FlatList
data={data}
renderItem={({ item }) => (
<Pressable
onPress={() => {
data.pop(); // Does not work because we are not changing it's ref
}}
style={{ backgroundColor: item, padding: 8 }}>
<Text>{item}</Text>
</Pressable>
)}
/>
</>
);
};
The data need to have a new array reference as below. data2.filter(..) will return a new array, we are not changing the data2 base values, just creating a new array with one item less.
const MyList = () => {
const [data2, setData2] = React.useState([
'#00FFFF',
'#0080FF',
'#0000FF',
]);
return (
<>
<Text>List WORKING!</Text>
<FlatList
data={data2}
renderItem={({ item }) => (
<Pressable
onPress={() => {
setData2(data2.filter(dataItem => dataItem !== item)) // works
//setData2([]); // Also works
}}
style={{ backgroundColor: item, padding: 8 }}>
<Text>{item}</Text>
</Pressable>
)}
/>
</>
);
};
A library like Immer.js simplify the manipulation of states to mutate the object, and immer will created a new reference for you.
Oh no rookie mistake that wasted everyones time!!
I was implementing shouldComponentUpdate method that was stopping Flatlist rendering :(
Thanks for all for the answers
You may need to use ListEmptyComponent, which is a prop that comes with FlatList, src.
Honestly, I'm not sure why it does not re-render when you update your state, or why they added a specific function/prop to render when the array is empty, but it's clear from the docs that this is what's needed.
You can do something like this:
<SafeAreaView style={styles.container}>
<FlatList
data={DATA}
renderItem={renderItem}
keyExtractor={(item) => item.id}
extraData={selectedId}
--> ListEmptyComponent={() => <MyComponent />}
/>
</SafeAreaView>

Dynamically populating nested react-native-collapsible

I need to populate a menu with items from an api request.
I made some sample items; const items. Some of the menu items have children, and the children can also have children, so I need to nest several levels of menu items.
I made an Accordion() with Collapsible from react-native-collapsible and an AccordionItem() for items that have no children.
function App() renders the menu items twice. Once by manually adding Accordions and AccordionItems and once by mapping items[]. RootMenuItem() is called for each top level item and renders that item and its sub-items by recursion.
When manually adding each item it works the way I want it to. I need to populate the menu programatically, but nested accordions rendered by RootMenuItem() are misbehaving on android and iOS. When testing in Web on snack.io it seems to be working fine.
Here is a snack with my complete App.js:
https://snack.expo.dev/#dissar/nested-collapsibles
Am I doing something wrong?
Does anybody have any tips for doing this in a better way?
PS: The dynamically rendered items have weird widths when testing on snack.io, but don't worry about that.
I seem to have fixed it myself by removing the View on line 46 and 56;
function RootMenuItem({item}){
if(item.children.length > 0) {
return(
<View style={{flex: 1}} key={item.id}> // <---- I removed this
<Accordion item={item} style={styles.menuItemView}>
{
item.children.map(child => (
<View style={{paddingLeft: 18}} key={child.id}>
<RootMenuItem item={child} style={{paddingLeft: 10}}/>
</View>
))
}
</Accordion>
</View> // <---- Also removed this
)
}
else return (
<AccordionItem item={item}/>
)
}
Not really sure though why that View made the nested accordions not work as they should. Please let me know if you have the answer.
I have a better solution without using any 3rd party library. This is completely customised and easy to understand. I used the same format of data as you used.
first of all, we have a component
const [active, setActive] = useState(null);
return (
<ScrollView
style={{ marginTop: 50 }}
contentContainerStyle={styles.container}>
{arr.map((x, i) => (
<Item
key={x.name}
active={active}
i={i}
setActive={setActive}
child={x.child}
/>
))}
</ScrollView>
);
then for the list items and their child
function Item({ i, active, setActive, child }) {
const onPress = () => {
LayoutAnimation.easeInEaseOut();
setActive(i == active ? null : I);
};
const [childActive, setChildActive] = useState(null);
const open = active == I;
return (
<TouchableOpacity style={styles.item} onPress={onPress} activeOpacity={1}>
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
<Text>Header - {i + 1}</Text>
{child.length ? <Text>{open ? 'close' : 'open'}</Text> : null}
</View>
{open &&
child.map((x, i) => {
if (x.child.length) {
return (
<Item
key={x}
active={childActive}
i={i}
setActive={setChildActive}
child={x.child}
/>
);
}
return (
<Text key={x} style={styles.subItem}>
- SOME DATA
</Text>
);
})}
</TouchableOpacity>
);
}
It's a completely dynamic process, you can extend the chain as long as you want. You can also check out the expo to look at its works.
https://snack.expo.dev/#akash0208/forlorn-popsicle

React native Flatlist not re-rendering on state change

I realize there are a lot of questions and answers about this out there but I am fairly new to react native and most of the answers are dealing with React Components and not hooks. In the following example availableInterests is pulled from a firestore database call. Then we loop through the availableInterests so the user can select the their interests from the Flatlist of interests. Everything works great except the FLatlist does not re-render so the button that is used to select currentInterests never shows the change that an interest has been selected. Does anyone see what I am missing here?
const [availableInterests, setAvailableInterests] = useState([]);
const [currentInterests, setCurrentInterests] = useState([]);
const selectThisInterest = (item) => {
let myInterests = currentInterests;
if(myInterests.includes(item.id)) {
myInterests.pop(item.id);
} else {
myInterests.push(item.id);
}
setCurrentInterests(myInterests);
}
return <View>
<Text style={styles.text}>Select Your Interests:</Text>
<FlatList
data={availableInterests}
keyExtractor={(item, index) => index.toString()}
extraData={currentInterests}
renderItem={({ item, index }) =>
<View key={item.id}>
<Text>{item.title}</Text>
<Text>{item.description}</Text>
<Image
source={{ uri: item.icon }}
style={{ width: 100, height: 100}}
/>
<TouchableOpacity onPress={() => selectThisInterest(item)}>
<Text style={styles.buttonText}>{`${currentInterests.includes(item.id) ? 'UnSelect' : 'Select'}`}</Text>
<Text>{item.id}</Text>
</TouchableOpacity>
</View>
}>
</FlatList>
</View>
put this state below
const [currentInterests, setCurrentInterests] = useState([]);
const [extra, setExtra] = useState(0);
at the end of your function just put this
const selectThisInterest = (item) => {
....
setExtra(extra + 1)
}
I think the mistake is in your selectThisInterest function. When you are updating the currentInterests based on previous value, React doesn't recognises such a change because you are simply assigning myInterests with your currentInterests.
What you want to do is to copy that array and assign it to myInteresets and then update your values to the new copied array. Once the calculation are completed on the new myInteresets array, the setCurrentInterests() will re-render the app because now React recognises there is a change in the state.
To copy the array, you can use,
let myInterests = [...currentInterests];
change your selectThisInterest function to reflect this change,
const selectThisInterest = (item) => {
let myInterests = [...currentInterests];
if(myInterests.includes(item.id)) {
myInterests.pop(item.id);
} else {
myInterests.push(item.id);
}
setCurrentInterests(myInterests);
}

How can I set data in a state in FlatList?

I want to save product ID (item.id) in a state like productId.
I need to product Id for add product to cart.
When I click on TouchableOpacity working fine but productId always is 4.
I have three Item. The id of last item is 4 and first item is 2.
When I click on TouchableOpacity of product 1, id is 4 but should be 2.
I see IDs are OK When I print IDs in listView.
<FlatList
data={this.state.dataSource}
renderItem={({item}) =>
<View>
<View>
<Text>{item.title} - {item.type}</Text>
<Text>{item.id}</Text>
<TouchableOpacity onPress={this.decrementCount,()=>this.setState({productId:item.id})}>
<AntDesign name="minus" size={15} style={{color:'#fff'}}/>
</TouchableOpacity>
<TouchableOpacity onPress={this.incrementCount} activeOpacity={0.5}>
<AntDesign name="plus" size={15} style={{color:'#fff'}}/>
</TouchableOpacity>
</View>
</View>
}
/>
I think this line is your issue:
<TouchableOpacity onPress={this.decrementCount,()=>this.setState({productId:item.id})}>
First things first, you should define your render function on your class, and you should define your onPress function separate from that. For example:
class MyClass extends React.Component {
handlePress = (item) => {
this.decrementCount()
this.setState({productId: item.id})
}
renderItem = ({item, index}) => {
return (...code from above, but using this.handlePress rather than your onPress function as currently defined)
}
render() {
return (
<FlatList
data={this.state.dataSource}
renderItem={this.renderItem}
/>
)
}
}
I think the real reason that its not working for you right now is that you're trying to call this.setState as a callback in your onPress function. It would probably work as is if you changed your current onPress from:
onPress={this.decrementCount,()=>this.setState({productId:item.id})}
to
onPress={() => this.setState({productId:item.id}, this.decrementCount)}
as I'm fairly sure you can have a function called as a callback from this.setState.
Hope this helps!

Make VirtualizedList show as Grid

I'm trying to make something like this:
The problem: The project was built with immutablejs and according to React Native Docs, I can't use FlatList thus I can't use numColumns props feature of that component.
AFAIK, my only choice is to use VirtualizedList as the docs points out, but I can't figure out how to display the cells as a grid as shown above.
I've already tried to add style props in both cell and view wrapper, but none of the code used to align the cells, like the picture I posted, is ignored. In fact it was showing perfect when I was using ScrollView, but due the HUGE lag I'm moving the code to VirtualizedList.
Any help? Anything would be welcome, I already digged a lot on Google but I can't find anything about this.
Some sample code:
<View>
<VirtualizedList
data={props.schedules}
getItem={(data, index) => data.get(index)}
getItemCount={(data) => data.size}
keyExtractor={(item, index) => index.toString()}
CellRendererComponent={({children, item}) => {
return (
<View style={{any flexbox code gets ignored here}}>
{children}
</View>
)}}
renderItem={({ item, index }) => (
<Text style={{also here}} key={index}>{item.get('schedule')}</Text>
)}
/>
</View>
Answering my own question:
I got it working by copying the FlatList.js source code from react-native repo.
Here's an example code:
<VirtualizedList
data={props.schedules}
getItem={(data, index) => {
let items = []
for (let i = 0; i < 4; i++) {
const item = data.get(index * 4 + i)
item && items.push(item)
}
return items
}}
getItemCount={(data) => data.size}
keyExtractor={(item, index) => index.toString()}
renderItem={({item, index}) => {
return (
<View key={index} style={{flexDirection: 'row'}}>
{item.map((elem, i) => (
<View key={i}>
<Text key={i}>{elem.get('horario')}</Text>
</View>
))}
</View>
)
}}
/>
The number 4 is for the number of columns. The key parts are in the getItem and adding flexDirection: 'row' at renderItem in the View component.