ListView removing wrong row on dataSource update - reactjs

I have a React Native ListView that seems to be removing the wrong row in the UI when I update the state with the new sections & rows - The row I deleted stays, but the one, or sometimes even, 2 below that gets removed until I reload the app.
What am I doing wrong?
constructor(props) {
super(props)
this.state = {
dataState: DataState.STATE_NOT_LOADED,
dataSource: new ListView.DataSource({
rowHasChanged: (r1, r2) => r1.identifier !== r2.identifier,
sectionHeaderHasChanged: (s1, s2) => s1 !== s2
})
};
}
componentDidMount() {
this.loadData();
}
loadData() {
this.setState({
dataState: DataState.STATE_LOADING
});
User.getDocs()
.bind(this)
.then(results => {
var docs = _.groupBy(results, function (d) {
return d.createdAt.format('MMMM D');
});
this.setState({
dataState: DataState.STATE_LOADED,
dataSource: this.state.dataSource.cloneWithRowsAndSections(docs)
});
})
.catch(error => {
console.log(error);
this.setState({
dataState: DataState.STATE_ERROR
});
});
}
onTrackPressed(doc) {
User.untrackDoc(doc)
.bind(this)
.tap(() => {
this.loadData();
});
}
renderRow(result) {
return (
<DocRow
style={styles.row}
doc={result}
tracked={true}
onPress={() => this.onRowPressed(result)}
onTrackPress={() => this.onTrackPressed(result)}/>
);
}

I'm not sure if this is the proper answer, but it's worked for me until I can find a better explanation/better answer.
I recently had this same question, as did someone else I was talking to. They fixed it by adding a key property to the ListView. Apparently, you need to add key to ListView because the table rows are super dynamic, and it seems like not having the key on each row was causing ListView to incorrectly choose what row to get deleted.
<ListView
key={putSomethingUniqueInHere}
dataSource={this.state.dataSource}
renderRow={this.renderRow.bind(this)}
/>

Related

React .map only returning first item

I've just started my React journey recently. I am currently trying to render properties of an array of objects which is returned from my controller.
The json:
[
{
"reportID":4,
"reportDescription":"Commission Bonus Register",
"reportNotes":"",
"reportName":"CommissionBonusRegister"
},
{
"reportID":5,
"reportDescription":"Reset Government ID",
"reportNotes":"",
"reportName":"ResetGovtID"
},
{
"reportID":6,
"reportDescription":"Distributor Chase Up Report",
"reportNotes":"",
"reportName":"DistributorChaseUpReport"
},
{
"reportID":7,
"reportDescription":"Vietnam Distributor Export",
"reportNotes":"",
"reportName":"VietnamDistributorExport"
},
{
"reportID":8,
"reportDescription":"Vietnam Order Export",
"reportNotes":"",
"reportName":"VietnamOrderExport"
},
{
"reportID":9,
"reportDescription":"Distributor List by status and period",
"reportNotes":"",
"reportName":"DistributorsList"
}
]
React component code:
constructor(props) {
super(props);
this.state = {
linkscontent: [],
loading: true,
refresh: true
}
this.populateReportsLinks = this.populateReportsLinks.bind(this);
}
async componentDidMount() {
await this.populateReportsLinks();
}
render() {
let contents = this.state.loading
? <p><em>Loading...</em></p>
:
this.state.linkscontent.map(([reports], index) => {
return <li key={reports.reportID}>{reports.reportDescription}</li>
});
return (
<div>
<h1 id="tabelLabel" >Reports</h1>
<ul>
{contents}
</ul>
</div>
);
}
async populateReportsLinks() {
const response = await fetch('reports')
.then(res => res.json())
.then(data =>
this.setState({ linkscontent: [data], error: data.error || null, loading: false, refresh: !this.state.refresh }));
return response;
}
After two days of frustration I have finally managed to get the first item to display, but only the first item. Ive read so many articles and forum solutions that seem to indicate this should work. Can anyone help me figure out what is wrong here?
Remove the [data] to just this.setState({ linkcontent: data, ...restOfUpdates }) after you have fetched your data.
While mapping don't destructure with [reports] just use the reports.
async componentDidMount() {
await this.populateReportsLinks();
}
render() {
let contents = this.state.loading
? <p><em>Loading...</em></p>
: this.state.linkscontent.map((reports, index) => {
return <li key={reports.reportID}>{reports.reportDescription}</li>
});
return (
<div>
<h1 id="tabelLabel" >Reports</h1>
<ul>
{contents}
</ul>
</div>
);
}
async populateReportsLinks() {
const response = await fetch('reports')
.then(res => res.json())
.then(data =>
this.setState({ linkscontent: data, error: data.error || null, loading: false, refresh: !this.state.refresh }));
return response;
}
You have a few problems with your logic. Let's look at them one by one.
When you set up state with your data
this.setState({ linkscontent: [data],...}
So when you do the above its basically makes linkscontent an array but only of one length. That means on its first index you have an array of your data.
When you run map like this
this.state.linkscontent.map(([reports], index)
That means you want to iterate through each index of linkscontent but since you have only one index in linkscontent you will get only one item printed.
How to fix.
There are a few ways to fix it. You can try saving data into the state as per below code. This will make linkscontent an array with the data source.
this.setState({ linkscontent: [...data],...}
Or
this.setState({ linkscontent: data,...}
then run map like this
this.state.linkscontent.map((report, index) => <li key={report.reportID}>{report.reportDescription}</li>)
With your current version of setting linkscontent of one length, you can run your map like this as well
this.state.linkscontent.length && this.state.linkscontent[0].map((report, index) => ...)
Yeah it comes down to your state not the mapping function. spread the results into an array. this also happens quite often when you incorrectly mutate or update the state.

changing state of one list and calling method using checkboxes on another list to filter

Mods,
This is a continuation of an older question here. I started to do an edit to that question but things had progressed so far I was rewriting most of it. I am not intending to duplicate the question.
I have 2 mongo collections and collection A (symptoms) all have items from collection B (conditions) they are related to.
"_id" : ObjectId("5dc4bc92b6523a203423f2fa"),
"name" : "Cough",
"symptoms" : [
ObjectId("5dc4bc19299dfc46843a65f0"),
ObjectId("5dc4bc19299dfc46843a65f2")
]
and vice versa
"_id" : ObjectId("5dc4bc19299dfc46843a65f0"),
"name" : "Lung Cancer",
"description" : "blah blah string",
"symptoms" : [
ObjectId("5dc4bc92b6523a203423f2fa")
]
I have the states and functions
class Home extends React.Component {
state = {
conditions: [],
symptoms: [],
selectedSymptom: []
}
componentDidMount() {
this.getConditionsMethod();
this.getSymptomsMethod();
}
getConditionsMethod = () => {
API.getConditions()
.then(data => {
console.log(data);
data.data.sort((a, b) => a.name.localeCompare(b.name));
this.setState({
conditions: data.data
})
})
.catch(err => console.log(err))
};
getSymptomsMethod = () => {
API.getSymptoms()
.then(data => {
console.log(data);
data.data.sort((a, b) => a.name.localeCompare(b.name));
this.setState({
symptoms: data.data
})
})
.catch(err => console.log(err))
};
filterConditionsMethod = () => {
API.getConditions()
.then(data => {
console.log(data);
data.data.sort((a, b) => a.name.localeCompare(b.name));
this.setState({
selectedSymptom: data.data
})
})
.catch(err => console.log(err))
};
In my render() {return I have checkboxes with the symptoms on the page
<div className="doubleCol">
{this.state.symptoms.map(item => (
<ListItem key={item.ObjectID}>
<input type="checkbox" className="sympSelect" />
{item.name}
</ListItem>
))}
</div>
and I render the conditions here with a filter for which conditions are selected
<div className="doubleCol">
{this.state.conditions
.filter(condition => condition.symptoms.includes(this.state.selectedSymptom))
.map(item => (
<ListItem key={item.ObjectID}>
{item.name}
</ListItem>
))}
</div>
So what I can't figure out is A) How to have the onMount which renders all the conditions do what it is supposed to do first so the filtering only happens after a checkbox is checked.
and B) How to make the checkbox change filter data in filterConditionsMethod by ObjectID
Sorry, I don't think I really got what you meant by "make the checkbox change filter data in filterConditionsMethod by ObjectID" so the B) question is kind of confusing to me.
As for the A) question, unfortunately I can't test it right now, but maybe I could help you if I got it right. You could add another statement for your filter when rendering conditions to check if any symptom is already selected, and to really filter it by symptoms if that's true. Something like this would do:
.filter(condition => !this.state.selectedSymptom.length || condition.symptoms.includes(this.state.selectedSymptom))
This way, the filter would only return false if there is at least one symptom selected (has length > 0) and it does not include one of these selected symptoms. If the first statement returns true (!0 = true), the filter will return true either way because of the OR logical operator.

Duplicate lists printing after adding new document to firestore collection

When the component loads, it pulls all the data from a specific collection in firestore and renders it just fine. then when i add a new document, it adds that document but then prints them all out (including the new one) under the previous list.
This is my first real react project and I am kinda clueless. I have tried resetting the state when the component loads and calling the method at different times.
import React, { Component } from 'react';
// Database Ref
import Firebase from '../../Config/Firebase';
// Stylesheet
import '../View-Styles/views.scss';
// Componenents
import Post from '../../Components/Post/Post';
import EntryForm from '../../Components/EntryForm/EntryForm';
export class Gym extends Component {
constructor() {
super();
this.collection = 'Gym';
this.app = Firebase;
this.db = this.app.firestore().collection('Gym');
this.state = {
posts: []
};
this.addNote = this.addNote.bind(this);
};
componentDidMount() {
this.currentPosts = this.state.posts;
this.db.onSnapshot((snapshot) => {
snapshot.docs.forEach((doc) => {
this.currentPosts.push({
id: doc.id,
// title: doc.data().title,
body: doc.data().body
});
});
this.setState({
posts: this.currentPosts
});
});
};
addNote(post) {
// console.log('post content:', post );
this.db.add({
body: post
})
}
render() {
return (
<div className="view-body">
<div>
{
this.state.posts.map((post) => {
return(
<div className="post">
<Post key={post.id} postId={post.id} postTitle={post.title} postBody={post.body} />
</div>
)
})
}
</div>
<div className="entry-form">
<EntryForm addNote={this.addNote} collection={this.collection} />
</div>
</div>
)
};
};
export default Gym;
I am trying to get it to only add the new document to the list, rather than rendering another complete list with the new document. no error messages.
Your problem lies with your componentDidMount() function and the use of onSnapshot(). Each time an update to your collection occurs, any listeners attached with onSnapshot() will be triggered. In your listener, you add each document in the snapshot to the existing list. While this list starts off empty, on every subsequent change, the list is appended to with all of the documents in the collection (including the old ones, not just the changes).
There are two ways to handle the listener's snapshot when it comes in - either empty the existing list and recreate it on each change, or only handle the changes (new entries, deleted entries, etc).
As a side note: When using onSnapshot(), it is recommended to store the "unsubscribe" function that it returns (e.g. this.stopChangeListener = this.db.onSnapshot(...)). This allows you later to freeze the state of your list without receiving further updates from the server by calling someGym.stopChangeListener().
Recreate method
For simplicity, I'd recommend using this method unless you are dealing with a large number of items.
componentDidMount() {
this.stopChangeListener = this.db.onSnapshot((snapshot) => {
var postsArray = snapshot.docs.map((doc) => {
return {
id: doc.id,
// title: doc.data().title,
body: doc.data().body
});
});
this.currentPosts = postsArray;
this.setState({
posts: postsArray
});
});
};
Replicate changes method
This method is subject to race-conditions and opens up the possibility of desyncing with the database if handled incorrectly.
componentDidMount() {
this.stopChangeListener = this.db.onSnapshot((snapshot) => {
var postsArray = this.currentPosts.clone() // take a copy to work with.
snapshot.docChanges().forEach((change) => {
var doc = change.document;
var data = {
id: doc.id,
// title: doc.data().title,
body: doc.data().body
});
switch(change.type) {
case 'added':
// add new entry
postsArray.push(data)
break;
case 'removed':
// delete potential existing entry
var pos = postsArray.findIndex(entry => entry.id == data.id);
if (pos != -1) {
postsArray.splice(pos, 1)
}
break;
case 'modified':
// update potential existing entry
var pos = postsArray.findIndex(entry => entry.id == data.id);
if (pos != -1) {
postsArray.splice(pos, 1, data)
} else {
postsArray.push(data)
}
}
});
this.currentPosts = postsArray; // commit the changes to the copy
this.setState({
posts: postsArray
});
});
};
As a side note: I would also consider moving this.currentPosts = ... into the this.setState() function.
When you use onSnapshot() in Cloud Firestore,you can print only the added data. For your code, it should be something like:
snapshot.docChanges().forEach(function(change) {
if (change.type === "added") {
console.log("Newly added data: ", change.doc.data());
}
Also, Firestore does not load the entire collection everytime a new data is added, the documents are cached and will be reused when the collection changes again.
For more info, you can checkout this answer.

ReactJS+FireStore Data mapping issue

Im writing a small program to fetch the categories from the Firestore DB and show in webpage as a list.
My code look like this:
class Category extends Component {
constructor() {
super();
this.state = {'Categories': []}
}
render() {
let categoryList = null;
if (Array.isArray(this.state.Categories)) {
console.log(this.state.Categories);
categoryList = this.state.Categories.map((category) => {
return <li>{category.name}</li>
});
}
return(
<ul>{categoryList}</ul>
);
}
componentWillMount() {
// fetch the data from the Google FireStore for the Category Collection
var CategoryCollection = fire.collection('Category');
let categories = [];
CategoryCollection.get().then((snapshot)=> {
snapshot.forEach ((doc) => {
categories.push(doc.data());
});
}).catch((error) => {
console.log("Error in getting the data")
});
this.setState({'Categories': categories});
}
}
Im able to fetch the data and even populate this.state.Categories, however the map function is not getting executed.
The console.log statement produce an array of values butthe map function in render is not getting executed. Any thoughts?
Console.log output:
You have an error in handling data retrieval. In the last line categories is still empty, so it triggers setState with an empty data set. Should be something lie that
componentWillMount() {
fire.collection('Category').get()
.then(snapshot => {
const categories = snapshot.map(doc => doc.data());
// sorry, but js object should be pascal cased almost always
this.setState({ categories });
})
.catch(error => {
console.log("Error in getting the data")
});
}
Only return the data if the data exists. The simplest way to do this is to replace <ul>{categoryList}</ul> with <ul>{this.state.categories && categoryList}</ul>
I could make it work with a small change (moved this.setState to be inside the callback). Honestly, I still don't understand the difference.
P.S: I come from PHP development and this is my first step into ReactJS.
componentWillMount() {
// fetch the data from the Google FireStore for the Category Collection
var categoryCollection = fire.collection('Category');
let categories = [];
categoryCollection.get().then((snapshot)=> {
snapshot.forEach ((doc) => {
categories.push(doc.data());
});
if (categories.length > 0) {
this.setState({'Categories': categories});
}
}).catch((error) => {
console.log("Error in getting the data");
});
// this.setState({'Categories': categories});
}

Change data in row, that has been rendered by cloneWithRows

I want to implement something like facebook 'like' functionality on some posts, but I haven't understood what should I do in order to mutate the data from datasource, and re-render a specific row, based on user pressing a button, basically a counter.
This is my code:
export default class PrivateFeedList extends Component {
constructor(props) {
super(props);
var ds = new ListView.DataSource({
rowHasChanged: (r1, r2) => r1 != r2
})
this.state = {
datasource: ds.cloneWithRows( [] ),
refreshing: false
}
componentWillMount() {
this.fetchPrivateFeed()
}
_onRefresh() {
this.setState({refreshing: true});
this.fetchPrivateFeed()
.then(() => {
this.setState({refreshing: false});
});
}
render() {
return(
<View style= {styles.container}>
<ListView
refreshControl={
<RefreshControl
refreshing={this.state.refreshing}
onRefresh={this._onRefresh.bind(this)}
/>
}
dataSource={this.state.datasource}
renderRow={this.renderRow.bind(this)}
/>
</View>
)
}
fetchPrivateFeed() {
fetch('http://000.111.22.333:3000/', {
method: 'GET',
headers: {
'Authorization': 'id_token ' + token
}
})
.then((response) => response.json())
.then((feedItems) => {
console.log(feedItems)
this.setState({
datasource: this.state.datasource.cloneWithRows(feedItems)
});
})
}
renderRow(rowData) {
return(
<View style={styles.cardWrapper}>
<Text>{rowData.numberOfLikes}</Text>
{this.props.showLikeButton
? <Button onPress={()=> this.handleLikePress(rowData)}>
{this.hasUserLiked(rowData)
? <Text>LIKED</Text>
: <Text>LIKE!!</Text>
}
</Button>
: null
}
</View>
)
}
hasUserLiked(transaction) {
let result = false;
//userLiked is an array that contains all the usernames that have
liked the transaction. We cycle through this array, to check if the
present user is within the array, meaning that he has already liked
the transaction.
_.each(transaction.userLiked, function(userLikedItem) {
//has the user liked ? true or false
result = user.userName === userLikedItem;
})
return result;
}
handleLikePress(rowData) {
//Increase the numberOfLikes counter
//Push present user's username into the userLiked array.
//Re-render row.
}
I want to do the last 3 things I have written in the comments at the handleLikePress().
You do not mutate the dataSource instead you again populate the dataSource with new rows
handleLikePress(rowData) {
//Increase the numberOfLikes counter
//Push present user's username into the userLiked array.
this.setState({dataSource: this.state.dataSource.cloneWithRows(
newDataSource,
)});
}

Resources