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.
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'm trying to cascade a state change from a parent component down to a child.
export default class App extends React.Component {
constructor(props) {
super(props);
this.listUpdater = new_list_updater();
this.state = {
schoollist: this.listUpdater.update_list(),
}
}
listUpdateCallback = () => {
console.log("Callback triggered!");
console.log(this.state.schoollist.slice(1,3));
this.setState({schoollist: this.listUpdater.update_list()});
console.log(this.state.schoollist.slice(1,3));
}
render() {
return (
<SafeAreaView style={styles.container}>
<Header updateCallback={this.listUpdateCallback}/>
<SchoolList school_list={this.state.schoollist}/>
</SafeAreaView>
);
}
}
listUpdater.update_list() is a method in a class that implements getISchools and ShuffleArray and stores the list of schools that are being shuffled. It returns the shuffled list of schools.
import { shuffleArray } from './Shuffle'
import { getISchools } from './iSchoolData'
class ListUpdater{
constructor() {
console.log("Initiating class!");
this.currentSchoolList = [];
}
update_list() {
console.log("Updating list!");
if (this.currentSchoolList.length == 0){
this.currentSchoolList = getISchools();
}
else{
shuffleArray(this.currentSchoolList);
}
return(this.currentSchoolList);
}
}
export function new_list_updater(){
return new ListUpdater();
}
As far as I can tell everything works. When I press a refresh button in the Header component, it triggers the updateCallback, which updates the list stored in the state variable (verified by logging to console and ComponentDidUpdate()
This is the Component not refreshing:
import React from 'react';
import { StyleSheet, Text, View, SafeAreaView, FlatList } from 'react-native';
export default class SchoolList extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<SafeAreaView style={styles.listArea}>
<FlatList
data = {this.props.school_list}
renderItem = {({item}) =>
<View style={styles.row}>
<View style={styles.num_area}>
<Text style={styles.num_text}>{item.key}</Text>
</View>
<View style={styles.text_area}>
<Text style={styles.univ_text}>{item.univ}</Text>
<Text style={styles.school_text}>{item.school}</Text>
</View>
</View>
}
/>
</SafeAreaView>
);
}
componentDidUpdate(){
console.log("SchooList Updated!");
}
}
The flow I'm expecting is:
Parent passes updateCallback reference to Header (child)
Refresh button in Header triggers updateCallback in Parent
updateCallback in Parent updates state with setState
Parent and relevant children that use state variable re-render, displaying new list
1-3 appear to be working, 4 is not!
Maybe your componenet is not re-rendering when you use setState for some reason. Try adding a warn in the render method to check this. I also noticed you are mutating the array this.currentSchoolList, winch is passade as reference for your state (all objects are passed as refence). Try replaceing this making a copy of the array beforing calling shuffleArray(this.currentSchoolList).
You can copy the array this way (this is ES6 sintax): newArray = [...oldArrray];
Or using other methods.
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 understand that the wording of the question is slightly nebulous, so I will expand. This is a personal project of mine that I have taken up to learn some React basics and familiarize myself with socket.io
I have a CollapsibleList component, and a NestedList component, which renders an array of the CollapsibleList components.
NestedList has an event handler that gets set in componentWillMount of the component. The event is when a menu arrives via socket.io from my server. When the menu arrives, a new CollapsibleList is added to the array, and state is changed to trigger a rerender. The events are triggered by an initial socket.io event that is emitted via componentDidMount (get-menus).
CollapsibleList is collapsed/uncollapsed by its onclick which uses a toggleVisiblity method passed via props from the NestedList, whose state determines whether its child CollapsibleList components are open or not.
Problem: CollapsibleList props (which come from state of the NestedList) don't change on changing state of said NestedList. I have examined the properties in the debugger and I have been stuck for days. In other words, the CollapsibleList element appears on the browser window, but clicking it only changes the state of the NestedList, and the props of the CollapsibleList doesn't change, and thus it doesn't appear/disappear. I think it has something to do with creating the CollapsibleLists in the socket.io callback, bound with 'this', since the 'collapsed' prop of the CollapsibleList depends on this.state[restaurantId].collapsed. Source is below, if it is unclear I can add more explanation.
class CollapsibleList extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<List>
<ListItem
button
onClick={() => {
this.props.collapseEventHandler(this.props.restaurantId);
}}
>
<ListItemText primary="Collapse me!" />
</ListItem>
<ListItem>
<Collapse in={!this.props.collapsed} timeout="auto" unmountOnExit>
<ListItemText primary="Hello World!" />
</Collapse>
</ListItem>
</List>
);
}
}
class NestedList extends React.Component {
constructor(props) {
super(props);
//let menuData = props.menuData.map()
this.toggleVisiblity = this.toggleVisiblity.bind(this);
this.arrayOfMenus = [];
}
componentWillMount() {
socket.on(
"menu-arrived",
function(menuJson) {
if (menuJson.response.menu.menus.items) {
let restaurantId = menuJson.restaurant_id;
//let menuId = menuJson.response.menu.menus.items[0].menuId;
this.arrayOfMenus.push(
<CollapsibleList
collapsed={this.state[restaurantId].collapsed}
collapseEventHandler={this.toggleVisiblity}
restaurantId={restaurantId}
key={restaurantId}
/>
);
this.setState(function(prevState, props) {
return {
[restaurantId]: {
collapsed: prevState[restaurantId].collapsed,
updated: true
}
};
});
}
}.bind(this)
);
}
componentDidMount() {
getNearbyRestaurantRawData().then(
function(rawData) {
let restaurantIds = parseOutVenueIds(rawData);
let menusOpen = {};
for (let i = 0; i < restaurantIds.length; i++) {
menusOpen[restaurantIds[i]] = {
collapsed: true
};
}
this.setState(menusOpen, () => {
socket.emit("get-menus", {
ids: restaurantIds
});
});
}.bind(this)
);
}
toggleVisiblity(restaurantId) {
this.setState(function(prevState, props) {
let newState = Object.assign({}, prevState);
newState[restaurantId].collapsed = !prevState[restaurantId].collapsed;
return newState;
});
}
render() {
return (
<List>
<React.Fragment>
<CssBaseline>{this.arrayOfMenus}</CssBaseline>
</React.Fragment>
</List>
);
}
}
You are pushing CollapsibleList React elements to an array on the instance, which means that new React elements will not be created and returned from the render method when state or props change.
You should always derive your UI from state and props in the render method instead.
Example
class NestedList extends React.Component {
state = { restaurantIds: [] };
componentWillMount() {
socket.on("menu-arrived", menuJson => {
if (menuJson.response.menu.menus.items) {
let restaurantId = menuJson.restaurant_id;
this.setState(prevState => {
return {
restaurantIds: [...prevState.restaurantIds, restaurantId],
[restaurantId]: {
collapsed: prevState[restaurantId].collapsed,
updated: true
}
};
});
}
});
}
// ...
render() {
return (
<List>
<React.Fragment>
<CssBaseline>
{this.state.restaurantIds.map(restaurantId => (
<CollapsibleList
collapsed={this.state[restaurantId].collapsed}
collapseEventHandler={this.toggleVisiblity}
restaurantId={restaurantId}
key={restaurantId}
/>
))}
</CssBaseline>
</React.Fragment>
</List>
);
}
}
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.