React Native ListView renderRow method not firing with proper dataSource - arrays

I have been using React Native for some time now and I have never had this problem before. Perhaps because usually my data object is in-fact an object where as currently it is an array of objects. It is crucial that it remains an array so that I can maintain the correct order. This is for a messenger app so once I have sorted the messaged by date I NEED it to stay that way.
The Problem
The new message in the conversation will NOT render! I use REDUX/Firebase to sync with my remote DB and when I send a message through the app I am listening for the new messages and then updating the state. I am not using the child_appended event but the value event because I want the entire inbox of messages on a change. All of this happens correctly and without errors.
The dataSource object in state updates just fine, I can do a row count and see that it updates correctly. I can also look to the last object in the incoming array and see my current message just added with the right text and date. However when stepping through the renderMessageRow function, I can see that every row renders EXCEPT the new row. WTF... When I pause or print inside this row rendering function, the parameters just stop at the object right before the new message. However, the real fun begins when I print out this.state.dataSource from inside the renderMessageRow method. When you do this, THE NEW MESSAGE IS RIGHT THERE!! I can see it and the row count shows it has increased by one in the dataSource object. lol
I have tried changing this implementation so many ways, adding in a ton of slices or spread operators to make sure it's not the same array going in as it was previously in state before the change. Nothing is working. What's weirder is that this operation worked fine before I changed the dataSource coming in from REDUX from an object of objects (can't maintain order) to an array of objects. When it was an object of objects the new message always showed up just in random places throughout the list...
The Code
export default class SingleConvoView extends Component {
//rN Lifecycle ----------------------
constructor(props) {
super(props);
this.dataProtocol = new ListView.DataSource({
rowHasChanged: (row1, row2) => row1 !== row2,
});
this.state = {
conversationListViewHeight: 0,
dataSource: this.dataProtocol.cloneWithRows(this.props.messages),
};
}
componentWillReceiveProps(nextProps, nextState) {
if(deepEqual(this.props.messages, nextProps.messages, {strict: true}) === false) {
//This fires every time the REDUX state changes without any problems at all
//The messages property here has the new message appended to it
//The row count shows 1 more showing than before sending the message
this.updateDataSource(nextProps.messages);
}
}
componentDidReceiveProps(nextProps, nextState) {
//Tried this just incase, didn't work, commented out...
//this.state.dataSource.rowShouldUpdate('s1', (nextProps.messages.length - 1));
}
render() {
return (
<View style={[
styles.container,
{
width: this.props.display.width,
height: this.props.display.height,
}
]}>
{ this.renderConversation() }
</View>
);
}
renderConversation() {
if(this.state.dataSource.getRowCount() > 0) {
return (
<View style={{ height: (this.props.display.height - headerBarHeight - 50) }}>
<ListView
onLayout={event => {
// console.log('on layout event: new content size is: ', event.nativeEvent.layout.height);
this.setState({ conversationListViewHeight: event.nativeEvent.layout.height });
}}
onContentSizeChange={(newWidth, newHeight) => {
let totalContentHeight = newHeight - this.state.conversationListViewHeight + headerBarHeight;
if(this.state.conversationListViewHeight === 0 || newHeight < this.state.conversationListViewHeight) totalContentHeight = 0;
this.conversationScrollViewRef.scrollTo({ x: 0, y: totalContentHeight, animated: false });
}}
scrollEnabled={true}
removeClippedSubviews={false}
dataSource={this.state.dataSource}
renderRow={this.renderMessageRow.bind(this)}
pageSize={this.state.dataSource.getRowCount()}
ref={ref => { this.conversationScrollViewRef = ref; }}
renderScrollComponent={this.renderScrollComponent.bind(this)} />
</View>
);
} else {
return null;
}
}
renderScrollComponent(props) {
return (
<ScrollView
contentContainerStyle={{ paddingBottom: 20 }}
style={[
styles.conversationBox,
{ width: this.props.display.width - mainContainerSideMargins }
]} />
);
}
renderMessageRow(message, sectionID, rowID, highlightRow) {
let messageUser = message.userMessage ? 'You' : (this.props.senderFirstName || 'no name'),
messageTime = '', messageTitle = '';
if(message.hasOwnProperty('created')) {
let currentSentDate = new Date(message.created);
messageTime = `${currentSentDate.toLocaleDateString('en-US', DATE_DISPLAY_OPTIONS)}`;
}
messageTitle = message.userMessage ? `${messageTime}: ${messageUser}` : `${messageUser}: ${messageTime}`;
return (
<View style={styles.messageRow}>
<Text style={[
bodyFontStyle,
styles.messageOwnerHeader,
{
color: message.userMessage ? brand_blue_color : primary_color,
alignSelf: message.userMessage ? 'flex-end' : 'flex-start',
}
]}>
{ messageTitle }
</Text>
<View
shadowRadius={2}
shadowOpacity={1}
shadowColor={'rgba(0, 0, 0, 0.4)'}
shadowOffset={{width: -1, height: 1}}
style={styles.messageBodyContainer}>
<Text style={[
styles.messageBody,
{ textAlign: message.userMessage ? 'right' : 'left' }
]}>
{ message.body }
</Text>
</View>
</View>
);
}
//Functionality ---------------------
updateDataSource(data) {
if(typeof data != 'undefined' || data != null) {
let tempData = data.slice();
this.setState({
dataSource: this.state.dataSource.cloneWithRows(tempData),
});
}
}
}

I have solved this for now by adding in the initialListSize prop as well. I can't imagine this being the permanent solution however since no one else has responded I am answering this for Google searchers. If anyone else gives a better answer I will remove this and give them the credits.
renderConversation() {
if(this.state.dataSource.getRowCount() > 0) {
return (
<View style={{ height: (this.props.display.height - headerBarHeight - 50) }}>
<ListView
onLayout={event => {
this.setState({ conversationListViewHeight: event.nativeEvent.layout.height });
}}
onContentSizeChange={(newWidth, newHeight) => {
let totalContentHeight = newHeight - this.state.conversationListViewHeight + headerBarHeight;
if(this.state.conversationListViewHeight === 0 || newHeight < this.state.conversationListViewHeight) totalContentHeight = 0;
this.conversationScrollViewRef.scrollTo({ x: 0, y: totalContentHeight, animated: false });
}}
initialListSize={this.state.dataSource.getRowCount()}
scrollEnabled={true}
removeClippedSubviews={false}
dataSource={this.state.dataSource}
renderRow={this.renderMessageRow.bind(this)}
pageSize={this.state.dataSource.getRowCount()}
ref={ref => { this.conversationScrollViewRef = ref; }}
renderScrollComponent={this.renderScrollComponent.bind(this)} />
</View>
);
} else {
return null;
}
}

Related

View background won't change after clicking it and using ternary

So I am displaying 5 stars. They are all gray on default when they are not selected. When I click on the star, I want the background color to change of the View but it is not changing after clicking it. Also, if I click star 3, I want the stars before star 3 to also change the background color. But nothing is working.
I used console.log to view the changed list when handleStarClicked runs and it saves the changes but not sure what is going on. Would really appreciate some help, please
const stars = [{ id: 0, selected: false }, { id: 1, selected: false }, { id: 2, selected: false }, { id: 3, selected: false }, { id: 4, selected: false }];
const [holdStars, setHoldStars] = useState(stars);
const handleStarClicked = (id) => {
for (let i = 0 ; i < holdStars.length; i++) {
if (i <= id) {
holdStars[i].selected = true;
} else {
holdStars[i].selected = false;
}
}
setHoldStars(holdStars);
}
return (
<FlatList
data={holdStars}
horizontal
scrollEnabled={false}
renderItem={({ item }) =>
<Pressable onPress={() => handleStarClicked(item.id)} className="ml-1 p-1 rounded-xl items-center justify-center" style={{ backgroundColor: item.selected ? "#6ECCAF" : "#CCCCCC" }}>
<Icon name="star" type="AntDesign" size={32} color="white" />
</Pressable>
}
/>
)
The issue here is that you are modifying the holdStars array directly in the handleStarClicked function and that is causing the issue.
Try this :
const handleStarClicked = (id) => {
const newHoldStars = [...holdStars]; // create a new copy of the holdStars array
for (let i = 0 ; i < newHoldStars.length; i++) {
if (i <= id) {
newHoldStars[i].selected = true;
} else {
newHoldStars[i].selected = false;
}
}
setHoldStars(newHoldStars);
}

Single selection item that saves the state (REACT-NATIVE)

I currently have 4 TouchableHighlights and my code looks as follows:
state:
this.state = {
selected: null,
selectedButton: ''
}
Touchable Highlight (they're all the same except for text)
<TouchableHighlight
onPress={this.selectedButton}
underlayColor='blue
style={[styles.box, { backgroundColor: (this.state.selected === true ? 'red' : 'blue') }]}>
<Text>1</Text>
</TouchableHighlight>
my functions:
selectedButton = () => {
this._handlePress('flag', '1')
this.setState({
selected: true,
});
};
_handlePress(flag, button) {
if (flag == 1) {
this.setState({ selected: true });
}
this.setState({ SelectedButton: button })
}
Current behaviour: Whenever I select one button, all become highlighted and cannot be unpressed.
Desired behaviour: I want only one button to be selected at the time with its state being saved somewhere.
Note: I could get the desired behaviour by creating 4 different functions that contain a different flag number, however, I'd like to get this done in the cleanest way possible.
Any guidance please?
Thank you
You can create an array of button texts, then use .map(), which provides the current index value, to iterate through them. For example:
render() {
const renderHighlight = (text, index) => (
<TouchableHighlight
onPress={() => {
if(this.state.selectedIndex === index) {
// "Unpress" functionality
this.setState({selectedIndex: undefined});
}
else {
this.setState({selectedIndex: index
}
})
style={this.state.selectedIndex === index ?
{backgroundColor: "#0F0" : {}}
>
<Text>{text}</Text>
</TouchableHighlight>;
);
const buttons = ["button 0", "button 1", "button 2"];
return buttons.map((text, i) => this.renderHighlight(text, i));
}

React-Name Array from Json with variable as key

I'm not 100% sure how to ask this question, and I'm fairly new to react-native. I have a list of categories with a count in them. In PHP I may do something like this:
$purchases['kayaks'] = 0;
$purchases['kayaks']++;
so it increments every time a kayak is sold for this example. or more specifically something like this:
$purchases[$categoryName]++;
I need to take the name of the category based on the user pressing which category they want and add a count to it, then store it in json format in AsyncStorage.
So I know I can do this:
{
"categories": [
{
"kayaks":"0"
}
]
}
And if I import that into "products" I can do products.categories.kayaks to retrieve "0" (or whatever the purchase count is), but I need kayaks to be able to be a variable based on a selection the user makes. so something more like products.categories[categoryName], or however the more optimal way to do that would be in react-native. What is the way (or if there is a more ideal way other than this) to accomplish having different category counts like this?
I hope that makes sense. Thanks in advance!
Here is a basic example written in react-native that uses AsyncStorage to read/write data and manipulates an object. Check out https://facebook.github.io/react-native/docs/asyncstorage for more info about AsyncStorage.
import React from 'react';
import { ActivityIndicator, AsyncStorage, StyleSheet, Text, TextInput, TouchableOpacity, View } from 'react-native';
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center'
},
input: {
minWidth: 100,
backgroundColor: 'red'
}
});
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
data: undefined,
categoryName: 'kayak' // default input value
};
}
componentDidMount() {
AsyncStorage.getItem('data').then(dataAsString => {
// if the data exists lets convert it into an Object we can easily edit
// else default to an empty object
this.setState({ data: dataAsString !== null ? JSON.parse(dataAsString) : {} });
});
}
handleIncrement = () => {
const { data: prevData, categoryName } = this.state;
const prevValue = prevData[categoryName];
// if the category doesn't exist the data lets default to 0
const newValue = prevValue !== undefined ? prevValue + 1 : 0;
this.setState({
data: {
...prevData, // keep all the previous data
[categoryName]: newValue // but set the selected category to the new value
}
});
};
handleSave = () => {
const { data } = this.state;
const dataAsString = JSON.stringify(data); // convert data into a string so AsyncStorage can save it properly
AsyncStorage.setItem('data', dataAsString); // save the data string
};
handleOnChangeText = categoryName => this.setState({ categoryName });
render() {
const { data, categoryName } = this.state;
return (
<View style={styles.container}>
{data === undefined ? (
<ActivityIndicator /> // While data is undefined (AsyncStorage is still doing getItem('data)) show a loading indicator
) : (
<React.Fragment>
<Text>{JSON.stringify(data, null, 2)}</Text>
<TextInput style={styles.input} value={categoryName} onChangeText={this.handleOnChangeText} />
<TouchableOpacity onPress={this.handleIncrement}>
<Text>Add/Increment</Text>
</TouchableOpacity>
<TouchableOpacity onPress={this.handleSave}>
<Text>Save</Text>
</TouchableOpacity>
</React.Fragment>
)}
</View>
);
}
}
export default App;
Note : Its best to use an object to store your data (as Cuong Tran Duc mentions) as you are indexing with category.
Hope this example was helpful.
assuse that your data get from json look like this
const data = {
"categories": [
{
"kayaks":"0"
},
{
"foo":"1"
},
{
"bar":"1"
}
]
}
const categories = data["categories"].reduce((acc, category) => (acc[Object.keys(category)[0]] = Object.values(category)[0], acc), {});
console.log(categories);
you could convert it into an object then access data by categories[categoryName] like you want
change Object.values(category)[0] to +Object.values(category)[0]
if you want to convert string to number

Increment value in react native

I'm getting data from a payload which has a total number of likes on each post. On the user screen, there's an icon for the user to like a post and what i want to achieve is when the user taps on it, the value show be increased to plus 1 against that particular post
VIEW:
{
posts.map((item, i) => {
return (
<View key={i} style={styles.user}>
<Card>
<ListItem
titleStyle={{ color: '#36c', fontWeight:'500' }}
titleNumberOfLines={2}
hideChevron={false}
roundAvatar
title={item.headline}
avatar={{uri:'https://s3.amazonaws.com/uifaces/faces/twitter/brynn/128.jpg'}}
/>
<Text style={{marginBottom: 10, fontSize:16, color:'#4a4a4a', fontFamily:'HelveticaNeue-Light'}}>
{item.text}
</Text>
<TouchableOpacity style={styles.likeContainer}>
<Text style={{fontSize:14}}>{item.likesCount}{"\n"}</Text>
<Icon
onPress={()=>onLikePost(item)}
name='md-thumbs-up'
type='ionicon'
iconStyle={[(item.isLiked=== true) ? styles.likedColor : styles.unLikedColor]}
/>
</TouchableOpacity>
</Card>
</View>
);
})
}
CONTAINER:
state = {
posts : [],
id: '',
user: ''
}
componentDidMount = () => {
const { navigation } = this.props;
this.setState({
id : navigation.getParam('id'),
user: navigation.getParam('user')
}, ()=> this.getData())
}
getData = () => {
const api = create({
baseURL: 'https://url.com/api',
headers: {'Accept': 'application/json'}
});
api.get('/groups/'+`${this.state.groupID}`+'/posts').then((response) => {
let data = response.data.data
this.setState({ posts: data });
console.log(JSON.stringify(this.state.posts))
})
}
onLikePost = (item) => {
item.likeCount = item.likeCount+1
}
You are storing posts data in state variable so use setState to update it. Use map and check for each post, whenever id (unique property of each post) matches to id of the clicked item, increase its likesCount otherwise return the same data.
Write it like this:
onLikePost = (item) => {
this.setState(prevState => ({
posts: prevState.posts.map(el => el.id === item.id? {...el, likesCount: el.likesCount+1} : el)
}))
}
Update: Put the check before updating the count value and change the isLiked bool also.
onLikePost = (item) => {
this.setState(prevState => ({
posts: prevState.posts.map(el => {
if(el.id === item.id) {
return {
...el,
isLiked: !el.isLiked,
likesCount: !el.isLiked? el.likesCount+1 : el.likesCount-1,
}
}
return el;
})
}))
}
Note: I am assuming each post has a key id unique value, if it doesn't exist then use any other unique property of the each post.
If array sequence is not an issue, you can use item index and use setState to update it.
<Icon
onPress={()=>onLikePost(i)}
...
/>
...
onLikePost = (i) => {
let posts = this.state.posts;
posts[i].likesCount = !posts[i].isLiked ? posts[i].likesCount + 1 : posts[i].likesCount - 1;
posts[i].isLiked = !posts[i].isLiked;
this.setState({ posts: posts})
}

How to update the array - ReactNative

What I'm trying to achieve is an Array like this
infos = [{type: 'phone', val: '2222222'}
{type: 'email', val: 'abc#abc.com'}]
Here is the state
constructor(props) {
super(props);
this.state = {
rows: [], //this is used for the add component
contactDetails: [{type: '', val: ''}], // where i will add the values
};
}
So I have a two TextInput
<View>
<TextInput label='Type'/>
<TextInput label='Details'/>
</View>
This View can be dynamically added when a button is clicked and can also be deleted. Here is my code for the add and delete button:
addRow(){
this.state.rows.push(index++)
this.setState({ rows: this.state.rows })
}
deleteRow(key){
this.state.rows.splice(key, 1)
this.setState({ rows: this.state.rows })
}
<TouchableOpacity onPress={ this.addRow.bind(this) } >
<Text style={{textAlign: 'center',}}>Add</Text>
</TouchableOpacity>
...
<TouchableOpacity onPress={ () => this.deleteRow(i) } >
<Image source={require('./../img/delete.png')} style={{width:32, height: 32 }} />
</TouchableOpacity>
How can I get the value of it correctly and add it to my Array?
Thank you.
Update
In my TextInput I tried doing this
<TextInput
onChangeText={(text)=>{
let cd = this.state.contactDetails;
cd[i].val = text ;
this.setState({cd});
}}
value={this.state.contactDetails[i]}
label='Details'
/>
but I'm always having an error undefined is not an object (evaluating 'cd[i].val = text')
You can't manipulate state by declaring this.state.rows.push(index++), but you must do it with concat() and return the value to new variable. For instance (I actually don't know what index represents in your case, but let's give it a try:
addRow() {
const rows = this.state.rows.concat(index++)
this.setState({rows})
}
Here's a working example what I assume you're trying to achieve: https://snack.expo.io/HySVtag6g
In my state, I added an array like this
info: {
type: '',
val: '',
}
And then I added this to my addRow
addRow(){
//add new array in contactDetails
var arrayvar = this.state.contactDetails.slice()
arrayvar.push(this.state.info)
this.setState({ contactDetails: arrayvar })
//add new component row
this.state.rows.push(index++);
this.setState({ rows: this.state.rows});
}
Where every time the add button is clicked the Array named info with empty values will be added to the contactDetails and then I update the values of the added array.

Resources