In my react-native app I have a function that changes the state of the pressed item from false to true, but the issue is that it's changing the state for all of the items not only the pressed one, i want when i press to change it for the pressed one only inside my FlatList, here is the code:
Initial state:
state={pressed: false}
Function:
changeItem = async item => {this.setState({ pressed: true });};
Rendering item and binding the function:
_renderItem({ item, index }) {
<TouchableOpacity onPress={this.changeItem.bind(this, item)}>
<Text> Click me </Text>
</TouchableOpacity>
);
}
FlatList:
<FlatList data={this.state.items}
keyExtractor={this._keyExtractor.bind(this)}
renderItem={this._renderItem.bind(this)}/>
The problem here is that you have a list of items, but all of then have the same state.
You need a list of items (an array) but you also need an array of the items state.
So instead of state = { pressed: false } you need state = { pressed: []}
And when rendering the items you need to pass the index of the pressed button:
_renderItem({ item, index }) {
return this.state.pressed[index] &&
<TouchableOpacity onPress={this.changeItem.bind(this, item, index)}>
<Text> Click me </Text>
</TouchableOpacity>
}
And Update the state only in the selected index
changeItem = async (item, index) => {
let itensPressed = this.state.pressed
itensPressed[index] = itensPressed[index] ? false : true
this.setState({ pressed: itensPressed })
}
But there is something better than doing this.
I see that you are getting the items from the state so maybe you want to update the items array an not create another variable.
This depends on how is your this.state.items is coming and if you can or can't have the isPressed value in that array.
If you show how your items is coming, I can create a better answer.
If you have multiple button that have its own state. you need to make an array pressed boolean, in state so that each element in an array has its own pressed state.
Do it like this.
import React, {Component} from 'react';
import { FlatList, TouchableOpacity, Text} from 'react-native';
sampleData = [
{key: 0, pressed: false},
{key: 1, pressed: false},
{key: 2, pressed: false}
]
export default class Example extends Component {
state={
data: sampleData
}
componentDidUpdate(){
console.log(this.state.data)
}
changeItem(item)
{
this.setState( (prevState) => {
prevState.data[item.key] = { ...item, pressed: !item.pressed}
return{
...prevState,
data: [...prevState.data]
}
});
};
_keyExtractor = (item, index) => item.key;
_renderItem({ item, index }) {
return(
<TouchableOpacity onPress={this.changeItem.bind(this,item)}>
<Text> Click me </Text>
</TouchableOpacity>
)
}
render() {
const {data} = this.state
return (
<FlatList
data={data}
keyExtractor={this._keyExtractor}
renderItem={this._renderItem.bind(this)}
/>
)
}
}
i just create an array of object contains key and pressed property and passed it to state.data
check console log, to see the output state.
Related
I'm building a React-Native app and trying to optimize it, i runned into the case of my Flatlist.
So this Flatlist basically renders few elements and each of these elements are selectable.
The issue i'm facing is that selecting one single item rerenders the whole Flatlist, and thus all items it contains.
I've seen a lot of solutions online already, and tried them without any success.
Here is my code :
Class component containing the Flatlist
const keyExtractor = (item) => item.id
export default class OrderedList extends Component {
state = {
selected: null,
}
onPressSelect = (id) => {
console.log(this.state.selected)
if(this.state.selected === id) {
this.setState({ selected: null})
}
else {
this.setState({ selected: id})
}
}
renderItemOrdered = ({item}) => {
const { group, wording, description, id: uniqueID } = item
const { id, name } = group
return (
<CategoryCard
type="ordered"
// item={item}
uniqueID={uniqueID}
groupName={name}
groupID={id}
description={description}
title={wording}
selected={this.state.selected}
onPressSelect={() => this.onPressSelect(item.id)}
/>
)
}
render() {
return (
<FlatList
initialNumToRender={10}
maxToRenderPerBatch={10}
data={this.props.data}
renderItem={this.renderItemOrdered}
keyExtractor={keyExtractor}
extraData={this.state.selected} ---> Tried with and without it
/>
)
}
}
Class component containing the renderItem method
export default class CategoryCard extends Component {
shouldComponentUpdate = (nextProps, nextState) => {
return nextProps.selected !== this.props.selected &&
nextProps.onPressSelect !== this.props.onPressSelect
}
render(){
if(this.props.type === 'ordered') {
return (
<Pressable style={this.props.selected === this.props.uniqueID ? styles.cardContainerSelected : styles.cardContainer} onPressIn={this.props.onPressSelect}>
<View style={[styles.cardHeader, backgroundTitleColor(this.props.groupID)]}>
<Text style={[styles.cardGroupName, textTitleColor(this.props.groupID)]}>{this.props.groupName}</Text>
</View>
<View style={styles.cardContent}>
<Text style={styles.cardTitle}>{this.props.wording}</Text>
<Text style={styles.cardDescription} numberOfLines={3} ellipsizeMode="tail">{this.props.description}</Text>
</View>
</Pressable>
)
}
}
}
What i already tried :
At first my components were functional components so i changed them into class components in order to make things works. Before that, i tried to use React.memo, also to manually add a function areEqual to it, to tell it when it should rerender, depending on props.
It didn't give me what i wanted.
I also tried to put all anonymous functions outside return statements, made use of useCallback, played around the ShouldComponentUpdate (like adding and removing all the props, the onPress prop, selected props)... None of that worked.
I must be missing something somewhere.. If you can help me with it, it would be a big help !
I have a screen with Items on it that's rendered in a flatlist. If I "onLongPress" on an item's text/title, I should navigate to a Details page with title, etc filled in. If I press the "+" button on the screen with all the Items, it should go to the Details page but with empty fields (to be able to create a new one). I'll show some code now.
Here's part of my screen that renders the items:
...
type RootStackParamList = {
ListDetails: { listName: string };
Item: { listName: string, itemInfo: any };
ItemDetails: { listName: string, itemInfo: any };
};
type ListDetailsScreenRouteProp = RouteProp<RootStackParamList, 'ListDetails'>;
type ListDetailsScreenNavigationProp = StackNavigationProp<RootStackParamList, 'ListDetails'>;
type Props = {
route: ListDetailsScreenRouteProp;
navigation: ListDetailsScreenNavigationProp
};
export default function DetailsScreen({ route, navigation }: Props) {
const { listName } = route.params;
...
const renderItem = (item: any) => (
<ItemScreen listName={listName} itemInfo={item} />
)
return (
<View style={styles.container}>
<Text style={styles.header}>{listName}</Text>
{data.length > 0 ? (
<FlatList
style={styles.flatlist}
data={data}
renderItem={({ item }: any) => renderItem(item)}
/>
) : (
<Text style={styles.noItems}>There are no items here yet!</Text>
)}
<FloatingActionButton onPress={() => navigation.navigate('ItemDetails', { listName: listName, itemInfo: null })} />
</View>
It will show a title/header, flatlist and then the FAB, that will navigate to 'ItemDetails' but without the itemInfo, so that the fields can be empty. Here i have to pass 2 props to 'ItemScreen'. The FAB navigate action WORKS, because in 'ItemDetails' I can access the data via 'route.params'.
What DOESN'T work, is the following in 'ItemScreen':
...
type RootStackParamList = {
Item: { listName: string, itemInfo: any };
ItemDetails: { listName: string, itemInfo: any };
};
type ItemScreenNavigationProp = StackNavigationProp<RootStackParamList, 'Item'>;
type Props = {
navigation: ItemScreenNavigationProp,
};
export default function ItemScreen({ listName, itemInfo}: any, { navigation }: Props) {
...
const onLongPress = () => {
navigation.navigate('ItemDetails', {
listName: listName,
itemInfo: itemInfo,
});
}
...
There's something wrong I believe in my destructered props/params in 'ItemScreen'. Since I'm using typescript, I also have to add '{ navigation }: Props' to the parameter list, and also my 2 params I want to send from my previous screen 'listName' and 'itemInfo'.
Code builds and shows no errors in this case, but when trying to use 'onLongPress', it will say 'navigation is undefined'.
I've tried putting my 2 params in the 'Props' object, but then in my previous screen he complains that '' needs a 'navigation' parameter/prop. But I don't want to pass that one, I want to use the implicit one from code, how it's done normally on all my other screens?
Does anyone know how to properly use ItemScreen params AND navigation so that navigation isn't undefined, without having to pass 'navigation' when renderen 'ItemScreen' components? Also it can't be route params I THINK, because they aren't passed by route but with parameter/props when rendereing the component via '<ItemScreen ... />'
I've got an array of object. I mapped it to render it like a list with a label and an icon button.
I need to show another View below the element of the row when I hit the button.
The array of object is a let variable. I would have like to make it as a state (because of the re-rendering when a function setState is executed), but I need it as a let variable because they are datas coming from a navigation route
My idea was to add a flag in every object of the array to indicate when the View has to be rendered. This would have been false at the beginning and true when I hit the button, but nothing happen. While if don't use a flag when I hit the button, the View is rendered in every row
state={
visible:false
}
getNewView = (item) => {
if (this.state.visible && item.showViewBelow)
return (
<View>
<HiddenViewComponent/>
</View>
)
};
btnHandler = (item) => {
item.showViewBelow = true;
this.setState({visible:true})
}
render(){
const {navigation} = this.props;
const arrayNav = navigation.getParam('data', '');
let array = [
{
id:'0',
label: arrayNav.label1,
showViewBelow: false
},
{
id:'1',
label: arrayNav.label2,
showViewBelow: false
},
{
id:'2',
label: arrayNav.label3,
showViewBelow: false
}
]
return(
<View>
array.map((item, key)=> { return
<View key = {key} style={{marginHorizontal: 25}}>
<Text>{item.label}</Text>
<TouchableOpacity onPress={this.btnHandler(item)}>
<Image source=(blablabla)/>
</TouchableOpacity>
{this.getNewView(item)}
</View>)
)}
</View>
)
}
So, when i click in a row of the list, I want a new view to be showed below THIS row.
I tried to switch to true the flag in one of the object and leave it to false in the others. It works fine on that line! When I click the button in that row, the new view is correctly rendered below that row.
I tried also using a Flatlist and put some extradata with a state variable to be updated with a setState after the return of the new view. (just to try to re-render the page).
Still, I tried to put this.forceUpdate() in the getNewView method. Nothing happen. Nothing is rendered
So I think the problem is I don't re-render the whole screen, because having the object as let variable, this doesn't happen.
Have any ideas?
You can create method in constant utility to define defaultArray like
getDefaultArray(arrayNav){
return [
{
id:'0',
label: arrayNav.label1,
showViewBelow: false
},
{
id:'1',
label: arrayNav.label2,
showViewBelow: false
},
{
id:'2',
label: arrayNav.label3,
showViewBelow: false
}
];
}
In your component code
import Constants from "./Constants"
ComponentDidMount(){
const {navigation} = this.props;
const arrayNav = navigation.getParam('data', '');
this.state={
visible:false,
array = Constants.getDefaultArray(arrayNav);
}
}
getNewView = (item) => {
if (this.state.visible && item.showViewBelow)
return (
<View>
<HiddenViewComponent/>
</View>
)
};
btnHandler = (item, index) => {
const {navigation} = this.props;
const arrayNav = navigation.getParam('data', '');
let newData = Constants.getDefaultArray(arrayNav);
newData[index].showViewBelow = true;
this.setState({visible:true , array:newData})
}
render(){
return(
<View>
this.state.array.map((item, key)=> { return
<View key = {key} style={{marginHorizontal: 25}}>
<Text>{item.label}</Text>
<TouchableOpacity onPress={this.btnHandler(item,key)}>
<Image source=(blablabla)/>
</TouchableOpacity>
{this.getNewView(item)}
</View>)
)}
</View>
)
}
I have a list of objects which I want to show in a FlatList. The list items should also have a switch to select or deselect the entry. But everytime I click the switch, the state stays unchecked. I don't know if this is a bug of the ListItem component or in my code. The onValueChange event is triggered correctly but then it seems that the FlatList is not rerendered after changing the state or the dataset is not updated...
Here are my classes:
class Person {
constructor() {
this._name = '';
this._selected = false;
}
getName() {
return this._name;
}
setName(value) {
this._name = value;
}
isSelected() {
return this._selected;
}
setSelected(value) {
this._selected = value;
}
}
class App extends Component {
constructor(props) {
super(props);
let p1 = new Person();
p1.setName('Superman');
let p2 = new Person();
p2.setName('Batman');
let list = [ p1, p2 ];
this.state = { persons: list };
}
renderItem = ({item, index}) => (
<ListItem
title={item.getName()}
switch={{
value: item.isSelected(),
onValueChange: (value) => {
let list = this.state.persons;
list[index].setSelected(value);
this.setState({ persons: list });
},
}}
/>
);
render() {
return (
<View style={styles.container}>
<FlatList
data={this.state.persons}
renderItem={this.renderItem}
/>
</View>
);
}
}
Your code works fine, only the extraData property of the FlatList component is missing.
Checkout the documentation:
By passing extraData={this.state} to FlatList we make sure FlatList
itself will re-render when the state.selected changes. Without setting
this prop, FlatList would not know it needs to re-render any items
because it is also a PureComponent and the prop comparison will not
show any changes.
In your case, it has to be:
<FlatList
data={this.state.persons}
renderItem={this.renderItem}
extraData={this.state}
/>
Here is a working demo.
I'm very new to react native and I'm trying to update list dynamically.
Below is my code:
import React, { Component } from "react";
import { View, Text, StyleSheet, FlatList } from "react-native";
import { Tile, ListItem, List } from "react-native-elements";
export default class JoinSession extends Component {
constructor() {
super();
this.state = {
dataToRender: [{ "id": "0", "name": "name0", "des": "des0" }]
}
}
componentDidMount() {
var counter = 0;
const interval = setInterval(() => {
try {
var temp = {
"id": ++counter + "",
"name": "name" + counter,
"des": "des" + counter
}
let tempArray = this.state.dataToRender;
tempArray.push(temp);
this.setState({
dataToRender: tempArray
});
console.log(this.state.dataToRender);
if(counter === 10) {
clearInterval(interval);
}
} catch (e) {
console.log(e.message);
}
}, 2000);
}
renderList(item) {
console.log(item);
return (
<ListItem
roundAvatar
title={item.name}
subtitle={item.des}
/>
);
}
render() {
return (
<View style={{ flex: 1, alignItems: "stretch", backgroundColor: "skyblue" }}>
<List>
<FlatList
data={this.state.dataToRender}
renderItem={({ item }) => this.renderList(item)}
keyExtractor={item => item.id}
/>
</List>
</View>
);
}
}
I am only able to get first element which I've declared in the constructor but data which I'm appending in serInterval is not showing up on the page.
Please help me to resolve it or if there is any other way to do it, please let me know.
Thanks in advance.
Hi try to have a look on the extraData parameter you can use on a FlatList:
By passing extraData={this.state} to FlatList we make sure FlatList itself will re-render when the state.selected changes. Without setting this prop, FlatList would not know it needs to re-render any items because it is also a PureComponent and the prop comparison will not show any changes.
<FlatList
...
extraData={this.state}
/>
More info on the FlatList documentation: https://facebook.github.io/react-native/docs/flatlist.html
Also you shouldn't need this <List> from react native element here the list behaviour is totally handle by your FlatList.
Like say #AlexDG Flat list is Pure component. For updating pure component use key prop.
But do not overdo it, otherwise you can get unnecessary redrawing.
<FlatList
key={this.state.dataToRender.length} <---------- rerender
data={this.state.dataToRender}
renderItem={({ item }) => this.renderList(item)}
keyExtractor={item => item.id}
/>
I just had this on my own and just so happened to read this comment by OP:
The problem was array mutation. Never mutate array or object in react native.
And I indeed changed my state this way:
this.setState(prev =>
prev.listData.push("stuff");
return prev;
});
And you can see it in the question as well:
let tempArray = this.state.dataToRender;
tempArray.push(temp);
After changing it to
this.setState(prev => {
let copy = Array.from(prev.listData);
copy.push("stuff");
return {listData: copy};
});
however, my list was updating just fine!
So if you are mutating an array in your state that is related to your list's data you might want to see if this helps some.