React Native: Dynamic state allocation inside a .map loop giving error - reactjs

My goal of this code:
Render some view elements with a loop
Inside the loop, set the state
On clicking the elements, update that value
Here is my code:
import React, {Component} from 'react';
import {View, Text, TouchableOpacity} from 'react-native';
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
maths: {},
};
}
prepareMaths = function() {
var count = 5;
var updateMath = key => {
var stateMaths = this.state.maths;
stateMaths['position_' + key] = Math.random();
this.setState({maths: stateMaths}, () => {
console.log(this.state.maths);
});
};
var stateMaths = this.state.maths;
return [...Array(count)].map((options, key) => {
stateMaths['position_' + key] = Math.random();
this.setState({maths: stateMaths});
return (
<TouchableOpacity
key={key}
onPress={() => updateMath(key)}
style={{
height: 100,
width: 200,
marginRight: 20,
marginBottom: 10,
backgroundColor: 'green',
}}>
<Text>{this.state.maths['position_' + key]}</Text>
</TouchableOpacity>
);
});
};
render() {
return (
<View>
<View>{this.prepareMaths()}</View>
</View>
);
}
}
I'm getting this error with this code:
I'm very confused. Because if I remove setState... code inside the loop, it's showing random maths naturally. But how? Since I'm using this.state.maths['position_' + key] on render. I really don't know how that data is generating.
Please help.
Thanks in advance.

Issues
State mutations
var stateMaths = this.state.maths; // <-- state
stateMaths['position_' + key] = Math.random(); // <-- mutation!!
Updating state in render function causes rerender. render is a pure function with zero side-effects
return [...Array(count)].map((options, key) => {
stateMaths['position_' + key] = Math.random();
this.setState({maths: stateMaths}); // <-- trigger rerender
Solution
Factor prepareMaths and updateMath into standalone utility functions
Convert maths state to array
Use componentDidMount to initialize state
Use componentDidUpdate to log updated state
Move the JSX from prepareMaths to render function for mapping from state
Updated component
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
maths: [],
};
}
componentDidMount() {
this.prepareMaths();
}
componentDidUpdate() {
console.log(this.state.maths);
}
updateMath = (key) => {
this.setState((prevState) => ({
maths: prevState.maths.map((el, i) => (i === key ? Math.random() : el)),
}));
};
prepareMaths = function () {
const count = 5;
this.setState({ maths: [...Array(count)].map(Math.random) });
};
render() {
const { maths } = this.state;
return (
<View>
<View>
{maths.map((value, key) => (
<TouchableOpacity
key={key}
onPress={() => this.updateMath(key)}
style={{
height: 100,
width: 200,
marginRight: 20,
marginBottom: 10,
backgroundColor: 'green',
}}>
<Text>{value}</Text>
</TouchableOpacity>
))}
</View>
</View>
);
}
}
Expo Snack Demo

Related

How to return the sum of all the dynamic TextInput fields in react native

import React, { Component } from 'react';
import {
Text,
View,
StyleSheet,
TouchableOpacity,
TextInput,
} from 'react-native';
class App extends Component {
constructor(props) {
super(props);
this.state = {
InputArr: [],
FinalVal: [],
Sum: 0,
};
}
AddNewView = () => {
this.setState({
InputArr: [
...this.state.InputArr,
<TextInput
style={styles.FieldNew}
keyboardType="numeric"
onChangeText={(e) =>
this.setState({ FinalVal: this.state.FinalVal.concat(e) })
}
/>,
],
});
};
CalBtnFunc = () => {
var Total = 0;
for (let i = 0; i <= this.state.FinalVal.length; i++) {
Total += Number(this.state.FinalVal[i]);
}
this.setState({ Sum: Total });
};
render() {
return (
<View style={styles.container}>
<TouchableOpacity style={styles.AddBtn} onPress={this.AddNewView}>
<Text>Click to add new Field</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.AddBtn} onPress={this.CalBtnFunc}>
<Text>Calculate</Text>
</TouchableOpacity>
<View>{this.state.InputArr}</View>
<Text>Sum is : {this.state.Sum}</Text>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#ecf0f1',
padding: 8,
},
AddBtn: {
marginTop: 30,
fontSize: 18,
fontWeight: 'bold',
textAlign: 'center',
},
FieldNew: {
borderColor: 'red',
borderWidth: 1,
marginBottom: 5,
},
});
export default App;
I'm trying to make a program where the user can add TextInput fields dynamically and once the user clicks on calculating it should return the sum of all the values in all the TextInput fields.
*TextInput will only accept numeric values.
I have written the code but it's not working as I want it to be.
What I have done:
when click to add new field is clicked it appends a TextInput field to the state variable called InputArr(Array) and all the values of the TextInput field are stored in a state variable called FinalVal. On clicking the calculate button I have used a for loop to iterate over all the values present in the FinalVal array and sum it and store it in another state variable called Sum.
Here's the Code Link (Snack) : Click to view code
I have changed AddNewView and CalBtnFunc to make it work.
Let's get through the changes I've made:
const CustomTextInput = ({name, _onChange}) => (
<TextInput
style={styles.FieldNew}
keyboardType="numeric"
onChangeText={(value) => {
_onChange(value, name)
}}
/>
);
Made a custom Component "CustomTextInput" to pass through it a name to be able to differentiate the different inputs that get added dynamically and a custom onChange function (_onChange).
AddNewView becomes:
AddNewView = () => {
this.setState({
InputArr: [
...this.state.InputArr,
<CustomTextInput
name={this.state.InputArr.length + 1}
_onChange={(value, name) => {
this.HandleChange(value, name)
}}
/>,
],
});
};
HandleChange = (value, name) => {
const newFinalVal = {...this.state.FinalVal};
newFinalVal[`${name}`] = value
if (!isNaN(value)) this.setState({ FinalVal: newFinalVal})
}
Inside the function that handles the change instead of having FinalVal as an array, I made it an object to store individually the value of each input. The object FinalVal will look something like this {"1": 10, "2": 7, ....}
CalBtnFunc = () => {
var Total = 0;
for (const [key, value] of Object.entries(this.state.FinalVal)) {
Total += Number(value);
}
this.setState({ Sum: Total });
};
Now since we have an array instead of an object, I changed CalBtnFunc to loop through it with for (const of) syntax instead of a normal loop.
Here's a working snack: https://snack.expo.dev/#whygee/grounded-bagel

React native VirtualizedList Re-render while scroll the list

I have Virtualized List initial render record up to 30 ,while render the data list automatically re render 2 to 4 times and also the new data added to the list
while rendering multi times we can't able to do any action like touch or navigate to another screen
My Code
class HomeDetails extends PureComponent {
constructor(props) {
super(props);
this.cellRefs = {};
this.flatListRef = React.createRef();
}
getItem = (data, index) => {
if (index in data) {
return {
key: `${data[index].id} - ${index}`,
id: data[index].id,
accountId: data[index].accountId,
displayName: data[index].displayName,
fullName: data[index].fullName,
};
}
};
keyExtractor(item, index) {
return `${item.id} - ${index}`;
}
getItemCount = data => {
return data.length;
};
_renderItem =({item,index}) => {
console.log(
'Rerendring',
item.accountId,
moment().format('MM/DD/YY hh:mm:ss a'),
);
return (
<View key={index} style={{height: 50, flexDirection: 'row'}}>
<Text>{`${item.accountId} ${moment().format(
'MM/DD/YY hh:mm:ss a',
)}`}</Text>
</View>
);
}
render(){
return (
<VirtualizedList
onScroll={this.onScrollHandler}
onViewableItemsChanged={this._onViewableItemsChanged}
viewabilityConfig={viewabilityConfig}
scrollEventThrottle={16}
ref={this.flatListRef}
horizontal={false}
decelerationRate="normal"
showsHorizontalScrollIndicator={false}
showsVerticalScrollIndicator={false}
data={this.props.responseRecord}
pagingEnabled={true}
scrollToOverflowEnabled={false}
renderItem={this._renderItem}
keyExtractor={this.keyExtractor}
getItemCount={this.getItemCount}
getItem={this.getItem}
windowSize={21}
progressViewOffset={20}
initialNumToRender={15}
maxToRenderPerBatch={15}
updateCellsBatchingPeriod={100}
onEndReached={val => {
return this.props.getExtraData(2, 1);
}}
onEndReachedThreshold={0.1}
refreshing={this.props.postLoading}
extraData={this.props.refreshData}
disableIntervalMomentum={false}
removeClippedSubviews={true}
onRefresh={() => {
return this.props.getExtraData(1, 1);
}}
ItemSeparator={this.ItemSeparator}
ListFooterComponent={this.renderFooter}
/>
)
}
}
const mapStateToProps = ({post, auth, common}) => {
const {
responseRecord,
postLoading,
refreshData,
} = post;
return {
responseRecord,
postLoading,
refreshData,
};
};
const mapDispatchToProps = {
getExtraData,
};
export default connect(mapStateToProps, mapDispatchToProps)(HomeDetails);
..........................................................................
1.For initial 30 record rendering its re-render more that 2 times
2.when add more records its re-render more than 4 to 6 times
3.tried with purecomponent but no luck
code deployed in snack
https://snack.expo.dev/#pandianvpsm/cd5737
Internal, React's PureComponent implements the shouldComponentUpdate method and compares previous props values and new props or state values to avoid unnecessary re-renders.
This works well for primitive type values like numbers, strings, and booleans.
For referential types values (objects and arrays), this comparison is not always accurate. This is the behavior you have. this.props.responseRecord is an array of objects (referential types).
We can solve this problem by implementing our own componentShouldUpdate method as below:
/** Trigger component rerender only new elements added */
shouldComponentUpdate(nextProps, nextState) {
return this.props.responseRecord.length !== nextProps.responseRecord.length
}
Full code below
class HomeDetails extends React.Component {
constructor(props) {
super(props);
this.cellRefs = {};
this.flatListRef = React.createRef();
}
/** Trigger component rerender only new elements added */
shouldComponentUpdate(nextProps, nextState) {
return this.props.responseRecord.length !== nextProps.responseRecord;
}
getItem = (data, index) => {
if (index in data) {
return {
key: `${data[index].id} - ${index}`,
id: data[index].id,
accountId: data[index].accountId,
displayName: data[index].displayName,
fullName: data[index].fullName,
};
}
};
keyExtractor(item, index) {
return `${item.id} - ${index}`;
}
getItemCount = (data) => {
return data.length;
};
_renderItem = ({ item, index }) => {
console.log(
"Rerendring",
item.accountId,
moment().format("MM/DD/YY hh:mm:ss a")
);
return (
<View key={index} style={{ height: 50, flexDirection: "row" }}>
<Text>{`${item.accountId} ${moment().format(
"MM/DD/YY hh:mm:ss a"
)}`}</Text>
</View>
);
};
render() {
return (
<VirtualizedList
onScroll={this.onScrollHandler}
onViewableItemsChanged={this._onViewableItemsChanged}
viewabilityConfig={viewabilityConfig}
scrollEventThrottle={16}
ref={this.flatListRef}
horizontal={false}
decelerationRate="normal"
showsHorizontalScrollIndicator={false}
showsVerticalScrollIndicator={false}
data={this.props.responseRecord}
pagingEnabled={true}
scrollToOverflowEnabled={false}
renderItem={this._renderItem}
keyExtractor={this.keyExtractor}
getItemCount={this.getItemCount}
getItem={this.getItem}
windowSize={21}
progressViewOffset={20}
initialNumToRender={15}
maxToRenderPerBatch={15}
updateCellsBatchingPeriod={100}
onEndReached={(val) => {
return this.props.getExtraData(2, 1);
}}
onEndReachedThreshold={0.1}
refreshing={this.props.postLoading}
extraData={this.props.refreshData}
disableIntervalMomentum={false}
removeClippedSubviews={true}
onRefresh={() => {
return this.props.getExtraData(1, 1);
}}
ItemSeparator={this.ItemSeparator}
ListFooterComponent={this.renderFooter}
/>
);
}
}
const mapStateToProps = ({ post, auth, common }) => {
const { responseRecord, postLoading, refreshData } = post;
return {
responseRecord,
postLoading,
refreshData,
};
};
const mapDispatchToProps = {
getExtraData,
};
export default connect(mapStateToProps, mapDispatchToProps)(HomeDetails);

How can i initialize component state with Props and make some changes to this state?

I have a screen which has a state property tags which is initialized with props - the props are an array of objects.
When set i use this.state.tags to render the object items in a FlatList which i then interact with by moving them around and swapping them using panResponder.
My problem is on the initial render the items displayed but i cannot move them but when i make a change in the code to force a re-render - then i can move the items - what could i be doing wrong.
class MyTags extends PureComponent<Props, State> {
props: Props;
state: State = {
tags: this.props.tags,
};
panResponder: PanResponder;
componentWillMount = async () => {
this.panResponder = this.createPanResponder();
await this.setState({tags: this.props.squadTemplate})
}
render() {
const {tags} = this.state
let myCards = [];
for (let i = 1; i < 5; i++) {
myCards.push({
id: i,
card: (
<CardHolder
tag={this.state.tags[i-1]}
/>
),
});
}
const slice1 = myCards.filter(card => card.card.props?.tag.new === true );
return (
<View style={styles.container}>
<FlatList
data={slice1}
renderItem={({item}) => (
<TouchableHighlight
style={{ marginHorizontal: 10, alignSelf: "center" }}
>
{item.card}
</TouchableHighlight>
)}
keyExtractor={item => item.id.toString()}
/>
</View>
)

FlatList renderItem not being highlighted when clicked the first time

Basically, I'm trying to setup a Flatlist in which multiple values can be selected.
My problem is with the styling of elements, when clicked the first time they don't get highlighted but when clicked the 2nd time they get highlighted.
FlatList Code
renderRow = ({item}) => (
<RowItem data={item} />
)
data = [
{
value: 'element1'
},
{
value: 'element2'
}
]
render(){
return (
<FlatList
data={this.data}
renderItem={this.renderRow}
keyExtractor={(item, index) => item + index}/>
)
}
RowItem Code
export default class RowItem extends React.Component {
state = {
isElementActive: false,
}
highlightElement = () => {
this.setState(prevState => ({
isElementActive: !prevState.isElementActive
}))
}
render() {
return (
<TouchableOpacity
activeOpacity={0.7}
onPress={this.highlightElement}
style={[styles.container, this.state.isElementActive ? styles.activeContainer : styles.inactiveContainer]}>
<Text>{this.props.data.value}</Text>
</TouchableOpacity>
)
}
}
const styles = Stylesheet.create({
container: {
height: 100,
width: 300,
backgroundColor: 'red',
},
activeContainer: {
opacity: 0.7,
},
inactiveContainer: {
opacity: 1,
}
});
When clicking on the element once, the value of the isElementActive returns true (when I console.log it) but the styling "activeContainer" does not apply. However, when I click it again, the styling applies even though the value of isElementActive then becomes false.
By default the value starts off as false, and they are not highlighted (i.e. have opacity of 1) and for this reason I'm kind of confused when clicked the first time, the value of isElementActive changes but the styling does not apply.
I was able to make it work with setOpacityTo after the setState.
Working example: https://snack.expo.io/SJNSKQPIB
import React from 'react';
import {TouchableOpacity, FlatList, StyleSheet, Text} from 'react-native';
type State = {
active: boolean;
};
type Props = {
value: string;
};
class RowItem extends React.Component<Props, State> {
state = {
active: null,
};
ref = null;
highlightElement = () => {
this.setState(
prevState => ({
active: !prevState.active,
}),
() => {
this.ref.setOpacityTo(this.state.active ? 0.7 : 1);
},
);
};
render() {
return (
<TouchableOpacity
ref={ref => (this.ref = ref)}
onPress={this.highlightElement}
style={[styles.container]}>
<Text>{this.props.value}</Text>
</TouchableOpacity>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
height: 100,
backgroundColor: 'red',
},
});
export default class App extends React.Component {
data = [
{
value: 'element1',
},
{
value: 'element2',
},
];
render() {
return (
<FlatList
keyExtractor={(_, index) => index.toString()}
data={this.data}
renderItem={({item}) => <RowItem value={item.value} />}
/>
);
}
}

Stacking Cards in react native

Currently I am learning react native and I have some problem while I am making stack of the cards.
Here is my code:
constructor(props) {
super(props);
const position = new Animated.ValueXY();
const panResponder = new PanResponder.create({
onStartShouldSetPanResponder: () => true,
onPanResponderMove: (event, gesture) => {
position.setValue({ x: gesture.dx, y: gesture.dy })
},
});
this.state = { panResponder, position, index: 0 };
}
renderCards() {
if(this.state.index >= this.props.data.length) {
return this.props.renderNoMoreCard();
}
return this.props.data.map((item, i) => {
if( i < this.state.index) { return null}
if(i === this.state.index) {
return (
<Animated.View
key={item.id}
style={[ styles.cardStyle, this.getCardStyle()]}
{...this.state.panResponder.panHandlers}
>
{this.props.renderCard(item)}
</Animated.View>
);
}
return (
<Animated.View key={item.id} style={styles.cardStyle}>
{this.props.renderCard(item)}
</Animated.View>
);
}).reverse();
}
render() {
return(
<View>
{this.renderCards()}
</View>
);
}
}
const styles = {
cardStyle: {
position: 'absolute',
width: SCREEN_WIDTH,
top: 0,
}
};
export default Deck;
The title card 2 is showing instead of card 1 but when I swipe the card. Card 1 is removed from view and title is changed to 3.
The mistake was that while using PanResponder which is use to reconciles several touches into a single gesture. So,
I was declaring a object const panResponder = new PanResponder.create
but the code should be
const panResponder = PanResponder.create and by Removing this single word "new" it worked!

Resources