Unable to update state with Array of objects in RN - javascript

I have a blank array in the state object. The array will contain some objects with key-value pair.
Here is my code:
import * as React from 'react';
import { Text, ScrollView, View, StyleSheet, TouchableOpacity, TextInput, ListView, Switch, Button } from 'react-native';
import { Constants } from 'expo';
export default class App extends React.Component {
constructor(props){
super(props);
this.ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2});
this.state = {
TodoTextVal: '',
todos: []
}
this.addTodo = this.addTodo.bind(this);
}
addTodo() {
id++;
let TodoTextVal = this.state.TodoTextVal;
//let arrVal = {id: id, text: TodoTextVal, checked: false}
//this.setState(prevState => ({todos : [...prevState.todos, arrVal]}));
this.setState({
todos: [
...this.state.todos,
{id: id, text: TodoTextVal, checked: false}
],
})
}
toggle(id) {
}
render() {
return (
<View style={styles.container}>
<View style={{flexDirection: "row"}}>
<TextInput
onChangeText={(TodoTextVal) => this.setState({TodoTextVal})}
ref= {(el) => { this.TodoTextVal = el; }}
style={[styles.input, {flex: 2} ]}
placeholder="Add Todo..."/>
<TouchableOpacity onPress={this.addTodo} title="+ Add" style={styles.button} >
<Text>Add</Text>
</TouchableOpacity>
</View>
<ListView
dataSource={this.ds.cloneWithRows(this.state.todos)}
renderRow={(data) => <View><Text>{data}</Text></View>} />
</View>
);
}
}
let id = 0
The problem here is this.setState() is not updating the state. I've tried both the method which you see in addTodo() that commented code too.
But both the methods are throwing an error with this message:
Device: (96:380) Invariant Violation: Objects are not valid as a React
child (found: object with keys {id, text, checked}).

First, stop using ListView. It's deprecated and second, update your renderRow method to render individual keys of the object instead of the complete object.

I think the problem is in:
renderRow={(data) => <View><Text>{data}</Text></View>}
Data is an object and you are trying to render it. JSX cannot render objects, instead select any key of data object to construct your html.

Related

Updating a value inside an array state through toggling a Switch Component on React Native

I have a signUp component in React Native (without expo) that could have multiple email inputs, Each of them is followed by a Switch component that indicate if this is the main email or not. I'm using react useState, to manage the behavior of the list of fields. but when I press the switch to toggle the value of the main attribute, the Switch stuck and doesnt move until I execute the next operation (in this example, I've created a button that insert a new item on array and made another dummy working Switch). but if I print the value, it is toggling normally as expected, but the component itself doesnt respond immediately. Here is my code so far:
import React, {useState} from 'react';
import {TextInput, View, Text, Switch, Button} from 'react-native';
export default function Signup() {
const [isEnabled, setIsEnabled] = useState(false);
const [emails, setEmails] = useState([
{
email: '',
main: true,
},
]);
const toggleSwitch = () => setIsEnabled(previousState => !previousState);
const setMainEmail = (value, emailIndex) => {
const array = emails;
console.log(array);
array[emailIndex].main = value;
console.log(array);
setEmails(array);
};
const addNewEmail = () => {
setEmails([
...emails,
{
email: '',
principal: false,
},
]);
};
return (
<>
<View>
<Text>Dummy Switch That Works</Text>
<Switch
trackColor={{false: '#767577', true: '#767577'}}
thumbColor={isEnabled ? '#FD7E77' : '#f4f3f4'}
ios_backgroundColor="#3e3e3e"
onValueChange={toggleSwitch}
value={isEnabled}
/>
</View>
<View>
{emails.map((email, emailIndex) => (
<View key={emailIndex}>
<TextInput
placeholder="Email"
autoCapitalize="none"
keyboardType="email-address"
/>
<View>
<Switch
value={email.main}
trackColor={{false: '#767577', true: '#767577'}}
thumbColor={email.main ? '#FD7E77' : '#f4f3f4'}
ios_backgroundColor="#3e3e3e"
onValueChange={event => setMainEmail(event, emailIndex)}
/>
<Text color="#fff">Main?</Text>
</View>
</View>
))}
<Button onPress={addNewEmail} title="+ Email" />
</View>
</>
);
}
Any help will be appreciated!
You just have to create a new array by spreading the previous array like this
const setMainEmail = (value, emailIndex) => {
const array = [...emails];
console.log(array);
array[emailIndex].main = value;
console.log(array);
setEmails(array);
};
This is the reason for the other scenario to work and this to fail, you are doing it right in the scenario where you add new items.

Objects are not valid as a React Child (found: object with keys{id, name})

im trying to implement radio buttons into my react native project with a callback from the component
Im using react-native-radio-buttons SegmentedControls into the project
App.js
import files....
import RadioButtons from "../components/RadioButtons";
//Data
const PackingType = [{id:"1", name: "Bag"},{id: "2",name: "Box"}];
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
packTypeSelected: [],
};
}
...
renderAddPackType(selectedOption, selectedIndex) {
this.setState({ selectedIndex });
console.log(selectedIndex[0]);
}
...
render(){
return(
...
<RadioButtons
buttonOptions={PackingType}
callback={this.renderAddPackType.bind(this)}
selectedOption={"Bag"}
/>
...
)
}
RadioButtons.js
import { SegmentedControls } from "react-native-radio-buttons";
export default class RadioButtons extends Component {
onSelectedItemsChange = (selectedOption, selectedIndex) => {
this.props.callback(selectedOption, selectedIndex);
};
render() {
return (
<View style={{ marginHorizontal: 20, marginVertical: 10 }}>
<SegmentedControls
options={this.props.buttonOptions}
onSelection={(selectedOption, selectedIndex) =>
this.onSelectedItemsChange(selectedOption, selectedIndex)
}
selectedOption={this.props.selectedOption}
/>
</View>
);
}
}
Error:
Invariant Violation: Invariant Violation: Objects are not valid as a React child (found: object with keys {id, name}). If you meant to render a collection of children, use an array instead.
im not much experienced in development so far..
kindly help with the mistakes done here
Thank you for your time
So, I was reading the documentation from react-native-radio-buttons and just found out that
You can also specify how to extract the labels from the options through the extractText prop
Which is clearly missing from your code. Here is what they expect you to do
<SegmentedControls
options={this.props.buttonOptions}
onSelection={(selectedOption, selectedIndex) =>
this.onSelectedItemsChange(selectedOption, selectedIndex)
}
selectedOption={this.props.selectedOption}
extractText={ (option) => option.name }
testOptionEqual={(selectedValue, option) => selectedValue === option.name}
/>
I haven't tried it but I think it would work
According to the error, SegmentedControls attribute options accept a ReactNode, and you assigning an array Object.
Change the PackingType type to ReactElement, for example:
// Array Object - throws an error
const PackingType = [{id:"1", name: "Bag"},{id: "2",name: "Box"}];
// ReactNode example
const PackingType = (
<div>
{[{ id: "1", name: "Bag" }, { id: "2", name: "Box" }].map(type => (
<div key={type.id}>{type.name}</div>
))}
</div>
);
// Assigning as props
<RadioButtons
buttonOptions={PackingType}
callback={this.renderAddPackType.bind(this)}
selectedOption={"Bag"}
/>
// Using them inside SegmentedControls
<View>
<SegmentedControls
options={this.props.buttonOptions}
/>
</View>
Note that I didn't check what SegmentedControls accepts in options attribute, it may be array of ReactNodes or something else, just recheck it because you assigning the wrong type.

Sending custom props to custom component is failing

I have created a custom component which takes a color name from the parent component and updates that color in the state in the parent component. Currently, after I have done all the code, it does not save the new color, and therefore, does not update the the state.
This is for a react-native android app that I am building. I have looked at the ReactNative documentation for flatlist and textinput. I have looked at Stack overflow for solutions too
Set up a react native project. this is my parent component
class HomePage extends Component {
constructor(props) {
super(props);
this.state = {
backgroundColor: "blue",
availableColors: [
{name: 'red'}
]
}
this.changeColor = this.changeColor.bind(this)
this.newColor = this.newColor.bind(this)
}
changeColor(backgroundColor){
this.setState({
backgroundColor,
})
}
newColor(color){
const availableColors = [
...this.state.availableColors,
color
]
this.setState({
availableColors
})
}
renderHeader = ()=>{
return(
<ColorForm onNewColor={this.newColor} />
)
}
render() {
const { container, row, sample, text, button } = style
const { backgroundColor, availableColors } = this.state
return (
<View style={[container,{backgroundColor}, {flex: 1}]} >
<FlatList
data={availableColors}
renderItem={
({item}) =>
<ColorButton
backgroundColor={item.name}
onSelect={(color)=>{this.changeColor(color)}}>
{item.name}
</ColorButton>}
keyExtractor={(item, index) => index.toString()}
ListHeaderComponent={this.renderHeader}
>
</FlatList>
</View>
);
}
}
this is the code for ColorForm component
class ColorForm extends Component {
constructor(props) {
super(props);
this.state = {
txtColor:'',
}
this.submit = this.submit.bind(this)
}
submit() {
this.props.onNewColor(this.state.txtColor.toLowerCase())
this.setState({
txtColor: 'yellow',
})
}
render() {
const {container, txtInput, button} = style
return (
<View style={container}>
<TextInput style={txtInput}
placeholder="Enter a color"
onChangeText={(txtColor)=>this.setState({txtColor})}
value={this.state.txtColor}></TextInput>
<Text
style={button}
onPress={this.submit}>Add</Text>
</View> );
}
}
and below is the code for ColorButton component
export default ({backgroundColor, onSelect=f=>f}) => {
const {button, row, sample, text} = style
return (
<TouchableHighlight onPress={()=>{onSelect(backgroundColor)}} underlayColor="orange" style={button}>
<View style={row}>
<View style={[sample,{backgroundColor}]}></View>
<Text style={text}>{backgroundColor}</Text>
</View>
</TouchableHighlight>
)
}
The imports and stylesheets are setup as standard and do not effect the code so I have chosen to not show them.
EDIT: Adding the expo snack here.
Expected Behavior:
When I press "ADD" on the ColorForm component, it should take that color and add that to the this.state.availableColor array and therefore visible in the ColorButton component. And when I touch the button, it should make that change
Current behaviour:
When I enter a color and press on add, it makes an empty button in the ColorButton component - NOT the color i entered in the color I entered in the ColorForm component.
EDIT: Adding the expo snack here.
Your state is updating but the FlatList is not updating. Because your data={availableColors} in flatlist is not changing but your state is changing .
Try to add extraData
A marker property for telling the list to re-render (since it implements PureComponent). If any of your renderItem, Header, Footer, etc. functions depend on anything outside of the data prop, stick it here and treat it immutably.
Try this
<FlatList
extraData={this.state.backgroundColor}
Updated Answer
the problem is in this function newColor(color)
const availableColors = [
...this.state.availableColors,
color
]
you just receive a string of color but you have defined object like this {name: 'red'}
please use this code
newColor(color){
const availableColors = [
...this.state.availableColors,
{name: color}
]
this.setState({
availableColors
})
}
Snack link with example : https://snack.expo.io/#mehran.khan/healthy-cake-state-sample
Also add export default to main component to remove error of launch
export default class HomePage extends Component {
App Preview
I had many problems using setState() in the way you are using now. I recommend you to use in this way setState(), with a callback:
this.setState((previousState, currentProps) => {
return { ...previousState, foo: currentProps.bar };
});
This is one of the article that talks about it.
setState() does not always immediately update the component. It may
batch or defer the update until later.
From react website setState().

Event Handler (prop) passed to child component cannot be called react native

I am passing an event handler showSpinner() from parent component. This method displays the activity Indicator in my app, the method when called from the parent class, works. But when I pass it down to a child component and then call it from the child as this.props.showSpinner(), I am getting the error
TypeError: undefined is not an object
(evaluating 'Object.keys(this.state.data)')
I am also not able to console.log the method at the child's props
Please note, I have already bound the function at the parent.
Here is a part of my code.
This is the parent component.
import React from 'react';
import { View, Button, Alert, Image, ScrollView, BackHandler, TouchableOpacity,Text, ActivityIndicator } from 'react-native';
import ProductListingItem from '../ProductCategories/ProductListingItemCategories.js';
import PusherColumnCategories from '../ProductCategories/PusherColumnCategories.js';
import NavRightButton from '../NavButton/NavRightButton.js';
import ActivitySpinner from '../ActivitySpinner.js';
const TAG = 'PRODUCTCATEGORIESPAGE';
export default class ProductCategoriesPage extends React.Component {
constructor(props) {
super(props);
/*this._getResponseFromApi = this._getResponseFromApi.bind(this);*/
this._onPressGoToCart=this._onPressGoToCart.bind(this);
if(props){
/*console.log(TAG,'constructor()','props available');*/
console.log(TAG,'constructor()','props JSON stringified = '+JSON.stringify(props));
/*this.setState({dataMain : (props.navigation.state.params.categories)});*/
}
this.state = {
dataMain: props.navigation.state.params.categories,
showIndicator: false,
};
console.log(TAG,'constructor','this.state.dataMain = '+this.state.dataMain );
}
static navigationOptions = ({navigation}) => {
return{
title: 'Categories',
headerLeft: null,
headerStyle: {
backgroundColor: '#EE162C',
},
/*headerBackTitleStyle: {
color: 'white',
},*/
headerTintColor: 'white',
headerRight: <NavRightButton navigation= {navigation}/>,
gesturesEnabled:false,
};
};
_onPressGoToCart(){
console.log(TAG,'_onPressGoToCart');
console.log(TAG,'_onPressGoToCart','navigation props ='+JSON.stringify(this.props));
const { navigate } = this.props.navigation;
navigate('CartPage');
}
componentWillReceiveProps(newProps){
console.log(TAG+'componentWillReceiveProps');
if(newProps){
console.log(TAG,'componentWillReceiveProps()','props available');
console.log(TAG,'componentWillReceiveProps()','props = '+newProps.navigation.state.params.categories);
}
}
_OnAlert(title,message){
console.log(TAG,'_onAlert');
Alert.alert(
title,
message,
[
{text:'done',onPress: () => { }}
]
);
}
componentDidMount () {
console.log(TAG,'componentDidMount');
/*this._getResponseFromApi();*/
BackHandler.addEventListener('hardwareBackPress',() => {return true});
}
componentWillMount () {
console.log(TAG,'componentWillMount');
}
_showSpinner(){
console.log(TAG,'_showSpinner');
this.setState({
showIndicator:true,
});
}
_hideSpinner(){
console.log(TAG,'_hideSpinner');
this.setState({
showIndicator:false,
});
}
render(){
console.log(TAG,'render');
console.log(TAG,'render','dataMain = '+this.state.dataMain[0].id);
// console.log(TAG,'render','showSpinner = '+JSON.stringify(this.showSpinner()));
// var tempshowspinner = this.showSpinner.bind(this);
// console.log(TAG,'render','tempshowspinner = '+JSON.stringify(tempshowspinner));
return(
<View
style={{
flex:1,
}}>
<ScrollView style = {{flex:1,
backgroundColor:'#F2F2F2',
}}>
<View style = {{
flex:1,
flexDirection:'column',
}}>
<PusherColumnCategories style = {{
flex:1,
}}
data = {this.state.dataMain}
navigate = {this.props.navigation}
showSpinner = {this._showSpinner}
hideSpinner = {this._hideSpinner}/>
</View>
</ScrollView>
<ActivitySpinner showIndicator={this.state.showIndicator}/>
</View>
);
}
}
This is the corresponding child component.
import React from 'react';
import {View, Component, Button} from 'react-native';
import ProductListingItem from './ProductListingItemCategories';
const TAG = "PUSHERCOLUMNCATEGORIES";
export default class PusherColumnCategories extends React.Component {
constructor(props){
super(props);
if(props){
console.log(TAG,'props ='+JSON.stringify(props));
/*console.log(TAG,'props data length = '+Object.keys(props.dataMain).length);*/
console.log(TAG,'Props = '+ JSON.stringify(props.data));
console.log(TAG,'Navigation Props = '+JSON.stringify(props.navigate));
}
this.state = {
data: props.data,
propsAvailable: false,
navigate: props.navigation,
};
};
componentDidMount(){
console.log(TAG,'componentDidMount');
}
componentWillReceiveProps(newProps){
console.log(TAG,'componentWillReceiveProps',newProps.data);
this.setState({
/*data: JSON.parse(JSON.stringify(newProps.data)),*/
data: (newProps.dataMain),
}, function() {
console.log(TAG,'componentWillReceiveProps','this.setState()','data = '+(Object.keys(this.state.data)));
});
}
componentDidUpdate(){
console.log(TAG,'componentDidUpdate');
}
render(){
console.log(TAG,'render()');
if(this.state.data){
console.log(TAG,'render()','state not empty');
console.log(TAG,'render()','data product_code = '+this.state.data[1].product_code);
return(
<View style = {{
flex:1,
flexDirection: 'column',
}}>
<Button
style = {{
flex:1,
}}
title = 'presshere'
onClick = {this.props.showSpinner}
/>
<RenderColumn style = {{
flex:1,
}}
data = {this.state.data}
navigate = {this.props.navigate}
showSpinner = {this.props.showSpinner}
hideSpinner = {this.props.hideSpinner}/>
</View>
);
} else {
console.log(TAG,'render()','state empty');
return(
<View style = {{
flex:1,
flexDirection: 'column',
}}/>
);
}
};
}
EDIT: I actually found the real problem. The binding stuff is good to know, but it's not the answer to the actual question. The problem is newProps.dataMain. newProps does not have a dataMain key on it, it's actually data as you can see below
<PusherColumnCategories style = {{
flex:1,
}}
data = {this.state.dataMain} // the key is `data`, not `dataMain`
navigate = {this.props.navigation}
showSpinner = {this._showSpinner}
hideSpinner = {this._hideSpinner}/>
So in this piece of code in componentWillReceiveProps
this.setState({
/*data: JSON.parse(JSON.stringify(newProps.data)),*/
data: newProps.data /* not newProps.dataMain */, // newProps.dataMain is not an actual key, so it will set `data` to undefined
}, function() {
console.log(TAG,'componentWillReceiveProps','this.setState()','data = '+(Object.keys(this.state.data))); // if you call `Object.keys` on undefined, you get the error that you posted
});
When you do myFunction.bind(obj), you create a new function that wraps your existing function and remembers the object obj you pass in, and whenever you call that new function, it calls your original function with this set to obj.
In your _showSpinner an _hideSpinner functions, you make use of this.setState, so it's important that this is set to your parent component so that you update the state of parent component, and you're trying to make sure of that, but you're also binding unnecessarily at a lot of places, like in the constructor of ProductCategoriesPage, which is unnecessary. And you also want to remove bind(this) from the following
tempshowspinner in the render function of PusherColumnCategories. this.props.showSpinner already updates the parent component since it's bound to it. So it's redundant binding it here again, which gives the wrong impression that you're binding it again to the child component, which is not the case.
The bind(this) in the Button component right below. You don't want to bind over here for the same reason

How to filter data in ListView React-native?

I am trying to filter my array object list and then trying to display in the ListView with new DataSource. However, the list is not getting filtered. I know that my filter function works correctly. ( I checked it in the console.log )
I am using Redux to map my state to prop. And then trying to filter the prop. Is this the wrong way?
Here is my code:
/*global fetch:false*/
import _ from 'lodash';
import React, { Component } from 'react';
import { ListView, Text as NText } from 'react-native';
import { connect } from 'react-redux';
import { Actions } from 'react-native-router-flux';
import {
Container, Header, Item,
Icon, Input, ListItem, Text,
Left, Right, Body, Button
} from 'native-base';
import Spinner from '../common/Spinner';
import HealthImage from '../misc/HealthImage';
import { assetsFetch } from '../../actions';
const ds = new ListView.DataSource({
rowHasChanged: (r1, r2) => r1 !== r2
});
class AssetsList extends Component {
componentWillMount() {
this.props.assetsFetch();
// Implementing the datasource for the list View
this.createDataSource(this.props.assets);
}
componentWillReceiveProps(nextProps) {
// Next props is the next set of props that this component will be rendered with.
// this.props is still equal to the old set of props.
this.createDataSource(nextProps.assets);
}
onSearchChange(text) {
const filteredAssets = this.props.assets.filter(
(asset) => {
return asset.name.indexOf(text) !== -1;
}
);
this.dataSource = ds.cloneWithRows(_.values(filteredAssets));
}
createDataSource(assets) {
this.dataSource = ds.cloneWithRows(assets);
}
renderRow(asset) {
return (
<ListItem thumbnail>
<Left>
<HealthImage health={asset.health} />
</Left>
<Body>
<Text>{asset.name}</Text>
<NText style={styles.nText}>
Location: {asset.location} |
Serial: {asset.serial_number}
</NText>
<NText>
Total Samples: {asset.total_samples}
</NText>
</Body>
<Right>
<Button transparent onPress={() => Actions.assetShow()}>
<Text>View</Text>
</Button>
</Right>
</ListItem>
);
}
render() {
return (
<Input
placeholder="Search"
onChangeText={this.onSearchChange.bind(this)}
/>
<ListView
enableEmptySections
dataSource={this.dataSource}
renderRow={this.renderRow}
/>
);
}
}
const mapStateToProps = state => {
return {
assets: _.values(state.assets.asset),
spinner: state.assets.asset_spinner
};
};
export default connect(mapStateToProps, { assetsFetch })(AssetsList);
What am I doing wrong here?
It's a little hard to follow what's going on here. I would simplify it to be like so:
class AssetsList extends Component {
state = {};
componentDidMount() {
return this.props.assetsFetch();
}
onSearchChange(text) {
this.setState({
searchTerm: text
});
}
renderRow(asset) {
return (
<ListItem thumbnail>
<Left>
<HealthImage health={asset.health} />
</Left>
<Body>
<Text>{asset.name}</Text>
<NText style={styles.nText}>
Location: {asset.location} |
Serial: {asset.serial_number}
</NText>
<NText>
Total Samples: {asset.total_samples}
</NText>
</Body>
<Right>
<Button transparent onPress={() => Actions.assetShow()}>
<Text>View</Text>
</Button>
</Right>
</ListItem>
);
}
getFilteredAssets() {
}
render() {
const filteredAssets = this.state.searchTerm
? this.props.assets.filter(asset => {
return asset.name.indexOf(this.state.searchTerm) > -1;
})
: this.props.assets;
const dataSource = ds.cloneWithRows(filteredAssets);
return (
<Input
placeholder="Search"
onChangeText={this.onSearchChange.bind(this)}
/>
<ListView
enableEmptySections
dataSource={dataSource}
renderRow={this.renderRow}
/>
);
}
}
const mapStateToProps = state => {
return {
assets: _.values(state.assets.asset),
spinner: state.assets.asset_spinner
};
};
export default connect(mapStateToProps, { assetsFetch })(AssetsList);
A few points:
Your component is stateful. There is one piece of state that belongs only to the component: the search term. Keep that in component state.
Don't change the data source in life cycle functions. Do it the latest point you know it's needed: in render.
I'm guessing that there's something async in assetFetch, so you probably should return it in componentDidMount.
I changed from componentWillMount to componentDidMount. It's recommended to put async fetching componentDidMount. This can matter if you ever do server side rendering.
Skip filtering if there is no search term. This would only matter if the list is very large.
One thing I have a little concern with is the pattern of fetching inside a component, putting it in global state, and then relying on that component to react to the global state change. Thus changing global state becomes a side effect of simply viewing something. I assume you are doing it because assets is used elsewhere, and this is a convenient point to freshen them from the server so that they will show up in other components that do not fetch them. This pattern can result in hard-to-find bugs.
You need to do setState to trigger render. Here's how I would do it -
constructor(props) {
super(props);
this.ds = new ListView.DataSource({ rowHasChanged: (r1,r2) => r1 !== r2 });
this.state = {
assets: []
};
}
componentWillMount() {
this.props.assetsFetch();
this.setState({
assets: this.props.assets
});
}
componentWillReceiveProps(nextProps) {
this.setState({
assets: nextProps.assets
});
}
onSearchChange(text) {
const filteredAssets = this.props.assets.filter(asset => asset.name.indexOf(text) !== -1);
this.setState({
assets: _.values(filteredAssets)
});
}
render() {
...
<ListView
dataSource={this.ds.cloneWithRows(this.state.assets)}
.....
/>
}

Categories