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.
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 avoid re-rendering unnecessary components
For example:
const[ValueState,SetValueState]=useState(5); //hook
<View>
<Text>{ValueState}</Text>
<View>
{
[...Array(3)].map((index,el)=>{
return (<View><Text>Hello there</Text></View>)
})
}
</View>
</View>
here every time I change the value of ValueState the entire map() segment also gets re-rendered
how do I avoid this and make the map() segment only render 1 time?
It depends on what the function you don't want to re-render depends on. In this case, your array and map function do not depend directly to ValueState.
One way to achieve 1 re-render is using React.memo
Example to render map function only once
import React, { useState } from "react";
import { View, Text, TouchableOpacity } from "react-native";
const ArrayMapSection = React.memo(()=> {
console.log("ArrayMapSection rendered")
return [...Array(3)].map((index,el)=>{
return (<View><Text>Hello there</Text></View>)
});
})
const App = () => {
const [ValueState,SetValueState]=useState(5); //hook
return(
<View>
<Text>{ValueState}</Text>
<TouchableOpacity onPress={()=>SetValueState(Math.random())}>Press to state update</TouchableOpacity>
<View>
<ArrayMapSection />
</View>
</View>
)
};
export default App;
If you run this program, you would see ArrayMapSection renderedonly once in the console. Now try to change the ValueState by pressing on Press to state update. The ArrayMapSection won't re-render because React.memo only re-renders if props changes
More info: https://reactjs.org/docs/react-api.html#reactmemo
Create a custom react component that takes the array as a prop and maps it to other components.
That way the component is only rerenderd if the array prop changes.
Code example:
const[ValueState,SetValueState]=useState(5);
<View>
<Text>{ValueState}</Text>
<CustomList array={[1,2,3]} />
</View>
export const CustomList = (array) => {
return (
<>
{
array.map((index,el)=>{
return (<View><Text>Hello there</Text></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.
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.
I have a horizontal flat list where each item is width:300
All I am trying to do is to get index of currently visible item.
<FlatList
onScroll={(e) => this.handleScroll(e)}
horizontal={true}
data={this.state.data}
renderItem...
Tried this:
handleScroll(event) {
let index = Math.ceil(
event.nativeEvent.contentOffset.x / 300
);
And something like this:
handleScroll(event) {
let contentOffset = event.nativeEvent.contentOffset;
let index = Math.floor(contentOffset.x / 300);
but nothing is accurate I always get one index up or one index down.
What am I doing wrong and how to get correct current index in flat list?
For example I get to list item that is 8th in a list but I get index 9 or 10.
UPD. thanks to #ch271828n for pointing that onViewableItemsChanged shouldn`t be updated
You can use FlatList onViewableItemsChanged prop to get what you want.
class Test extends React.Component {
onViewableItemsChanged = ({ viewableItems, changed }) => {
console.log("Visible items are", viewableItems);
console.log("Changed in this iteration", changed);
}
render () => {
return (
<FlatList
onViewableItemsChanged={this.onViewableItemsChanged }
viewabilityConfig={{
itemVisiblePercentThreshold: 50
}}
/>
)
}
}
viewabilityConfig can help you to make whatever you want with viewability settings. In code example 50 means that item is considered visible if it is visible for more than 50 percents.
Config stucture can be found here
With related to #fzyzcjy's and #Roman's answers. In react, 16.8+ you can use useCallback to handle the changing onViewableItemsChanged on the fly is not supported error.
function MyComponent(props) {
const _onViewableItemsChanged = useCallback(({ viewableItems, changed }) => {
console.log("Visible items are", viewableItems);
console.log("Changed in this iteration", changed);
}, []);
const _viewabilityConfig = {
itemVisiblePercentThreshold: 50
}
return <FlatList
onViewableItemsChanged={_onViewableItemsChanged}
viewabilityConfig={_viewabilityConfig}
{...props}
/>;
}
Many thanks to the most-voted answer :) However, it does not work when I try it, raising an error saying that changing onViewableItemsChanged on the fly is not supported. After some search, I find the solution here. Here is the full code that can run without error. The key point is that the two configs should be put as class properties instead of inside the render() function.
class MyComponent extends Component {
_onViewableItemsChanged = ({ viewableItems, changed }) => {
console.log("Visible items are", viewableItems);
console.log("Changed in this iteration", changed);
};
_viewabilityConfig = {
itemVisiblePercentThreshold: 50
};
render() {
return (
<FlatList
onViewableItemsChanged={this._onViewableItemsChanged}
viewabilityConfig={this._viewabilityConfig}
{...this.props}
/>
</View>
);
}
}
this.handleScroll = (event) => {
let yOffset = event.nativeEvent.contentOffset.y
let contentHeight = event.nativeEvent.contentSize.height
let value = yOffset / contentHeight
}
<FlatList onScroll={this.handleScroll} />
Get the round-off value to calculate the page-number/index.
In your case, I guess, you might ignore the padding or margin of items. The contentOffsetX or contentOffsetY should be firstViewableItemIndex * (itemWidth + item padding or margin).
As other answers, onViewableItemsChanged would be a better choice to meet your requirement. I wrote an article about how to use it and how it is implemented.
https://suelan.github.io/2020/01/21/onViewableItemsChanged/
A bit late, but for me was different with functional components...
If you are using functional component do it like:
const onScroll = useCallback((event: NativeSyntheticEvent<NativeScrollEvent>) => {
const slideSize = event.nativeEvent.layoutMeasurement.width;
const index = event.nativeEvent.contentOffset.x / slideSize;
const roundIndex = Math.round(index);
console.log("roundIndex:", roundIndex);
}, []);
Then on your FlatList
<FlatList
data={[2, 3]}
horizontal={true}
pagingEnabled={true}
style={{ flex: 1 }}
showsHorizontalScrollIndicator={false}
renderItem={renderItem}
onScroll={onScroll}
/>
While onViewableItemsChanged works for some conditions, it gives the
Error: Changing onViewableItemsChanged on the fly is not supported
error when ever you try to set the state.
This issue is fixed by using viewabilityConfigCallbackPairs instead of onViewableItemsChanged.
import React, {useRef} from 'react';
const App = () => {
// The name of the function must be onViewableItemsChanged.
const onViewableItemsChanged = ({viewableItems}) => {
console.log(viewableItems);
// Your code here.
};
const viewabilityConfigCallbackPairs = useRef([{onViewableItemsChanged}]);
return (
<View style={styles.root}>
<FlatList
data={data}
renderItem={renderItem}
keyExtractor={item => item.id}
viewabilityConfigCallbackPairs={
viewabilityConfigCallbackPairs.current
}
/>
</View>
);
}
Checkout this link for more details.
this will solve
Invariant Violation: Changing viewabilityConfig on the fly is not
supported
, and type script error on viewabilityConfigCallbackPairs.
const onViewableItemsChanged = ({ viewableItems }) => {
console.log(viewableItems)
};
const viewabilityConfig = { itemVisiblePercentThreshold: 100 };
const viewabilityConfigCallbackPairs = useRef([
{ viewabilityConfig, onViewableItemsChanged },
]);
<FlatList
...
viewabilityConfigCallbackPairs={
viewabilityConfigCallbackPairs.current
}
//viewabilityConfig={{ itemVisiblePercentThreshold: 100, }}<--------remove this
/>
typeScript:
import {ViewToken} from "react-native";
const onViewableItemsChanged = ({viewableItems}: {viewableItems: ViewToken[]}) => {
...
Kindly use class-based components. onViewableItemsChanged() method only works with classes otherwise it will give an error