Loading data from Firebase with React - reactjs

I have a firebase location with a list of patients
I am trying to reference this location and save it in the state of my react component to later pass to a drop down. I am pretty new to REACT, so i am not sure if this is this best way to this.
import * as firebase from "firebase";
class App extends Component {
constructor() {
super();
this.state = {
patient: []
};
}
componentDidMount() {
let patientList = "";
var rootRef = firebase
.database()
.ref()
.child("patient");
rootRef.on("child_added", snap => {
this.setState({
patient: snap.val().label
});
});
}
render() {
return (
<React.Fragment>
<li>{this.state.patient}</li>
</React.Fragment>
);
}
}
However this code is returning only the last label in the firebase location. Please help.

Try this one
rootRef.on("child_added", snap => {
// Store all the labels in array
var patients = [];
snapshot.forEach(function(snap){
var row = snap.val();
patients.push(row.label);
});
// Store label array into state
this.setState({
patient: patients
});
});

I think this will help your case to implement the real time patient list.
rootRef.on("child_added", snap => {
var patients = this.state.patient || [];
snapshot.forEach(snap => {
var row = snap.val();
patients.push(row.label);
});
this.setState({
patient: patients
});
I hope it helps. Thanks!!

Related

React Native: Duplicate items in component state

I have a two screens in a StackNavigator, one with a FlatList that displays data retrieved from Firestore, and another to add a new data to the database. After returning from the second screen in the stack via navigation.goBack(), the new item should be appended to the list. Instead, the entire state with the new item is being appended to the old state. The database data contains no duplicates and upon refresh, the list contains the correct elements.
I can't tell if I'm misunderstanding the component lifecycle or the query itself so I would appreciate any help.
export default class Main extends React.Component {
state = { chatData:[] }
componentDidMount = () => {
// Make call to Cloud Firestore
// for the current user, retrieve the chat document associated with each element in the chats id array
let user = firebase.auth().currentUser;
firestore().collection("users").doc(user.uid).onSnapshot((doc) => {
doc.data().chats.map((element) => {
firestore().collection("chats").doc(element).onSnapshot((doc) => {
this.setState({chatData: [...this.state.chatData,
{id: element, subject: doc.data().subject, course: doc.data().course}]})
})
});
})
}
state after adding a course and returning to the list screen (duplicate element)
When setting state try to use the prevState callback function. Like so:
export default class Main extends React.Component {
state = { chatData:[] }
componentDidMount = () => {
// Make call to Cloud Firestore
// for the current user, retrieve the chat document associated with each element in the chats id array
let user = firebase.auth().currentUser;
firestore().collection("users").doc(user.uid).onSnapshot((doc) => {
doc.data().chats.map((element) => {
firestore().collection("chats").doc(element).onSnapshot((doc) => {
// We use the parameter of the first argument of setState - prevState
this.setState(prevState => ({chatData: [...prevState.chatData,
{id: element, subject: doc.data().subject, course: doc.data().course}]}))
})
});
})
}
Because you want to spread the state that was there previously like an accumulator with the new data you're getting from firestore. If you do it with this.state then you'll be adding it again since it concerns the data that is already in the state and therefore repeated/duplicated. Let me know if it helps.
Try to create a new array with unique values and assign that to chatData
componentDidMount = () => {
let user = firebase.auth().currentUser;
firestore().collection("users").doc(user.uid).onSnapshot((doc) => {
doc.data().chats.map((element) => {
firestore().collection("chats").doc(element).onSnapshot((doc) => {
/**
* create a new array with unique values
*/
let newArray = [...this.state.chatData, { id: element, subject: doc.data().subject, course: doc.data().course }]
let uniqueArray = [...new Set(newArray)]
this.setState({
chatData: uniqueArray
})
})
});
})
}
Hope this helps you. Feel free for doubts.
Here is my eventual solution. I'm using react-navigation addListener to call the firestore API whenever the first screen is switched to and clearing the state when the second screen is navigated to. I also switched from onSnapshot() to get() for my firestore calls.
class Main extends React.Component {
state = { currentUser: null, chatData:[]}
componentDidMount = () => {
console.log('A - component did mount')
// definitely works
this.willFocusSubscription = this.props.navigation.addListener(
'willFocus',
payload => {
console.log('A- focus')
this.readCourseData()
})
this.willBlurSubscription = this.props.navigation.addListener(
'willBlur',
payload => {
console.log('A- blur')
this.setState({chatData: []})
})
}
componentWillUnmount() {
console.log('A - component will unmount')
this.willFocusSubscription.remove();
this.willBlurSubscription.remove();
// Remove the event listener
}
readCourseData = () => {
// Make call to Cloud Firestore
// for the current user, retrieve the chat document associated with each element in the chats array
let user = firebase.auth().currentUser;
firestore().collection("users").doc(user.uid).get().then((doc) => {
doc.data().chats.map((element) => {
firestore().collection("chats").doc(element).get().then((doc) => {
let newArray = [...this.state.chatData, { id: element, subject: doc.data().subject, course: doc.data().course }]
let uniqueArray = [...new Set(newArray)]
this.setState({
chatData: uniqueArray
})
})
});
})
}

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.

Parsing firebase query data in reactjs

I am using firebase cloud firestore for storing data. And I am developing a web app using reactjs. I have obtained documents using the following function:
getPeoples() {
let persons = [];
firestore.collection("students")
.get()
.then(function (querySnapshot) {
querySnapshot.forEach((doc) => {
var person = {};
person.name = doc.data().Name;
person.course = doc.data().Course;
persons.push(person);
})
});
console.log(persons);
return persons;
}
I am getting the desired data, but when I am iterating through persons array, it says it has length of 0.
here is the console output when I am displaying complete persons array and its length.
The length should be 14, but it shows 0. Please correct me what is wrong with me?
I want to display the output in the html inside the render() method of react component.
The output of
const peoples = this.getPeoples();
console.log(peoples);
It is:
The complete render method looks like:
render() {
const peoples = this.getPeoples();
console.log(peoples);
return (
<div className="peopleContainer">
<h2>Post-Graduate Students</h2>
{/* <h4>{displayPerson}</h4> */}
</div>
);
}
This is due to the fact the query to the database is asynchronous and you are returning the persons array before this asynchronous task is finished (i.e. before the promise returned by the get() method resolves).
You should return the persons array within the then(), as follows:
getPeoples() {
let persons = [];
return firestore.collection("students")
.get()
.then(function (querySnapshot) {
querySnapshot.forEach((doc) => {
var person = {};
person.name = doc.data().Name;
person.course = doc.data().Course;
persons.push(person);
})
console.log(persons);
return persons;
});
}
And you need to call it as follows, because it will return a promise :
getPeoples().then(result => {
console.log(result);
});
Have a look at what is written to the console if you do:
console.log(getPeoples());
getPeoples().then(result => {
console.log(result);
});
I'm not sure but please try to update your
getPeoples() {
let persons = [];
firestore.collection("students")
.get()
.then(function (querySnapshot) {
querySnapshot.forEach((doc) => {
var person = {};
person.name = doc.data().Name;
person.course = doc.data().Course;
persons.push(person);
})
});
console.log(persons);
return persons;
}
to
getPeoples() {
let persons = [];
firestore.collection("students")
.get()
.then(querySnapshot => {
querySnapshot.forEach((doc) => {
persons.push({name = doc.data().Name,
course = doc.data().Course
})
});
console.log(persons);
return persons;
}
Update
Sorry I thought you have problem with filling persons array in your function. Anyway as Renaud mentioned the query in your function is asynchronous so the result is not quick enough to be displayed on render. I use similar function and I found redux the best way to handle this situations.

Lifecycle hooks - Where to set state?

I am trying to add sorting to my movie app, I had a code that was working fine but there was too much code repetition, I would like to take a different approach and keep my code DRY. Anyways, I am confused as on which method should I set the state when I make my AJAX call and update it with a click event.
This is a module to get the data that I need for my app.
export const moviesData = {
popular_movies: [],
top_movies: [],
theaters_movies: []
};
export const queries = {
popular:
"https://api.themoviedb.org/3/discover/movie?sort_by=popularity.desc&api_key=###&page=",
top_rated:
"https://api.themoviedb.org/3/movie/top_rated?api_key=###&page=",
theaters:
"https://api.themoviedb.org/3/movie/now_playing?api_key=###&page="
};
export const key = "68f7e49d39fd0c0a1dd9bd094d9a8c75";
export function getData(arr, str) {
for (let i = 1; i < 11; i++) {
moviesData[arr].push(str + i);
}
}
The stateful component:
class App extends Component {
state = {
movies = [],
sortMovies: "popular_movies",
query: queries.popular,
sortValue: "Popularity"
}
}
// Here I am making the http request, documentation says
// this is a good place to load data from an end point
async componentDidMount() {
const { sortMovies, query } = this.state;
getData(sortMovies, query);
const data = await Promise.all(
moviesData[sortMovies].map(async movie => await axios.get(movie))
);
const movies = [].concat.apply([], data.map(movie => movie.data.results));
this.setState({ movies });
}
In my app I have a dropdown menu where you can sort movies by popularity, rating, etc. I have a method that when I select one of the options from the dropwdown, I update some of the states properties:
handleSortValue = value => {
let { sortMovies, query } = this.state;
if (value === "Top Rated") {
sortMovies = "top_movies";
query = queries.top_rated;
} else if (value === "Now Playing") {
sortMovies = "theaters_movies";
query = queries.theaters;
} else {
sortMovies = "popular_movies";
query = queries.popular;
}
this.setState({ sortMovies, query, sortValue: value });
};
Now, this method works and it is changing the properties in the state, but my components are not re-rendering. I still see the movies sorted by popularity since that is the original setup in the state (sortMovies), nothing is updating.
I know this is happening because I set the state of movies in the componentDidMount method, but I need data to be Initialized by default, so I don't know where else I should do this if not in this method.
I hope that I made myself clear of what I am trying to do here, if not please ask, I'm stuck here and any help is greatly appreciated. Thanks in advance.
The best lifecycle method for fetching data is componentDidMount(). According to React docs:
Where in the component lifecycle should I make an AJAX call?
You should populate data with AJAX calls in the componentDidMount() lifecycle method. This is so you can use setState() to update your component when the data is retrieved.
Example code from the docs:
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
error: null,
isLoaded: false,
items: []
};
}
componentDidMount() {
fetch("https://api.example.com/items")
.then(res => res.json())
.then(
(result) => {
this.setState({
isLoaded: true,
items: result.items
});
},
// Note: it's important to handle errors here
// instead of a catch() block so that we don't swallow
// exceptions from actual bugs in components.
(error) => {
this.setState({
isLoaded: true,
error
});
}
)
}
render() {
const { error, isLoaded, items } = this.state;
if (error) {
return <div>Error: {error.message}</div>;
} else if (!isLoaded) {
return <div>Loading...</div>;
} else {
return (
<ul>
{items.map(item => (
<li key={item.name}>
{item.name} {item.price}
</li>
))}
</ul>
);
}
}
}
Bonus: setState() inside componentDidMount() is considered an anti-pattern. Only use this pattern when fetching data/measuring DOM nodes.
Further reading:
HashNode discussion
StackOverflow question

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});
}

Resources