ComponentWillUnmount to unsubscribe from Firestore - reactjs

I'm trying to use ComponentWillUnmount to stop listening to Firebase Firestore collection changes:
https://firebase.google.com/docs/firestore/query-data/listen#detach_a_listener
var unsubscribe = db.collection("cities")
.onSnapshot(function (){
// Respond to data
// ...
});
// Later ...
// Stop listening to changes
unsubscribe();
However, I cannot access this unsubscribe(); since it is declared inside ComponentWillMount and I need to use it in ComponentWillUnmount.
How can I use this unsubscribe() inside ComponentWillUnmount? If I try to save it inside the state it throws an error that unsubscribe is not a function.
constructor() {
super();
this.state = {
notes: [],
unsubscribe: null
};
this.getNotes = this.getNotes.bind(this);
}
componentDidMount(){
this.getNotes();
}
componentWillUnmount(){
var unsubscribe = this.props.unsubscribe;
unsubscribe();
}
getNotes = () => {
const db = this.props.firestore;
const colRef = db.collection("users").doc(this.props.uid)
.collection("notes");
let notes = [];
const that = this;
// Realtime updates listener
var unsubscribe = colRef.orderBy("timestamp", "asc")
.onSnapshot(function(querySnapshot) {
var notes = [];
querySnapshot.forEach(function(doc) {
notes.push(
{ id: doc.id,
body: doc.data().body}
);
});
that.setState({ notes })
});
this.setState({ unsubscribe })
}
Throws:
Uncaught TypeError: unsubscribe is not a function

You can save the unsubscribe reference on the class instance (this): instead of doing var unsubscribe do this.unsubscribe = [...] and later just read it from the class instance again: this.unsubscribe()
componentDidMount(){
this.getNotes();
}
componentWillUnmount(){
this.unsubscribe();
}
getNotes = () => {
const db = this.props.firestore;
const colRef = db.collection("users").doc(this.props.uid)
.collection("notes");
let notes = [];
const that = this;
// Realtime updates listener
this.unsubscribe = colRef.orderBy("timestamp", "asc")
.onSnapshot(function(querySnapshot) {
var notes = [];
querySnapshot.forEach(function(doc) {
notes.push(
{ id: doc.id,
body: doc.data().body}
);
});
that.setState({ notes })
});
}

Related

Getting UNDEFINED values from Firestore onSnapshot + Promises for my React State

I'm trying to make a Tweets Application with React and Firebase and I have been suffering when trying to get info from more than 1 collection.
So this is the story:
I get the tweets using onSnapshot. All fine here
I need more info from 2 other collections: user_preferences and user_photo, so I use .get() within the onSnapshot
For managing asynchronism, I resolve my 2 promises before returning the tweet data + details data object for my map function.
I made a console.log of my mappedTweet and the values are OKEY. Here I can see the tweet data + details data
But my STATE "tweets" just have an array of undefined objects =(. It shows the right number of rows accoroding to my Tweets collection but rows of undefined data, and not the rows of my mappedTweets objects. Why?
Can anyone shed some light?
useEffect(() => {
//------------getting the TWEETS with onSnapshot()-------------
const cancelSuscription = firestore
.collection('tweets')
.onSnapshot((snapshot) => {
const promises = [];
const tweetsMapped = snapshot.docs.map((doc) => {
let tweetAndAuthor;
const tweetMappped = {
text: doc.data().text,
likes: doc.data().likes,
email: doc.data().email,
created: doc.data().created,
uid: doc.data().uid,
id: doc.id,
};
let authorPreference, authorPhoto;
const userPreferencePromise = firestore
.collection('user_preferences')
.where('uid', '==', tweetMappped.uid)
.get();
const userPhotoPromise = firestore
.collection('user_photos')
.where('id', '==', tweetMappped.uid)
.get();
promises.push(userPreferencePromise);
promises.push(userPhotoPromise);
//------------getting the AUTHOR USER PREFERENCES with .get()-------------
userPreferencePromise.then((snapshot2) => {
authorPreference = snapshot2.docs.map((doc) => {
return {
username: doc.data().username,
color: doc.data().color,
};
});
});
//------------getting the AUTHOR PHOTO with .get()-------------
userPhotoPromise.then((snapshot3) => {
authorPhoto = snapshot3.docs.map((doc) => {
return {
photoURL: doc.data().photoURL,
};
});
});
Promise.all(promises).then((x) => {
return {
...tweetMappped,
author: authorPreference[0].username,
authorColor: authorPreference[0].color,
authorPhoto: authorPhoto[0].photoURL,
};
});
});
Promise.all(promises).then((x) => {
setTweets(tweetsMapped);
});
});
return () => cancelSuscription();
}, []);
Well, I made it work by changing the model I was using to retrieve the data from Firebase.
I was using an outer onSnapshot with nested promises (I think I was very near here), but now I'm using nested onSnapshots and now app is behaving as expected.
So this is the new useEffect
useEffect(() => {
let cancelUserPrefSuscription, cancelUserPhotoSuscription;
// First onSnapshot
const cancelTweetSuscription = firestore
.collection('tweets')
.onSnapshot((tweetSnapshot) => {
const list = [];
tweetSnapshot.docs.forEach((tweetDoc) => {
//Second onSnapshot
cancelUserPrefSuscription = firestore
.collection('user_preferences')
.where('uid', '==', tweetDoc.data().uid)
.onSnapshot((userPrefSnapshot) => {
userPrefSnapshot.docs.forEach((userPrefDoc) => {
//Third onSnapshot
cancelUserPhotoSuscription = firestore
.collection('user_photos')
.where('id', '==', tweetDoc.data().uid)
.onSnapshot((userPhotoSnapshot) => {
userPhotoSnapshot.docs.forEach((userPhotoDoc) => {
//Taking the whole data i need from all snapshots
const newData = {
id: tweetDoc.id,
...tweetDoc.data(),
author: userPrefDoc.data().username,
authorColor: userPrefDoc.data().color,
authorPhoto: userPhotoDoc.data().photoURL,
};
list.push(newData);
//Updating my state
if (tweetSnapshot.docs.length === list.length) {
setTweets(list);
}
});
});
});
});
});
});
return () => {
cancelTweetSuscription();
cancelUserPrefSuscription();
cancelUserPhotoSuscription();
};
}, []);
Edit: Fix from comments of above code
Author: #samthecodingman
For each call to onSnapshot, you should keep track of its unsubscribe function and keep an array filled with the unsubscribe functions of any nested listeners. When an update is received, unsubscribe each nested listener, clear the array of nested unsubscribe functions and then insert each new nested listener into the array. For each onSnapshot listener attached, a single unsubscribe function should be created that cleans up the listener itself along with any nested listeners.
Note: Instead of using this approach, create a Tweet component that pulls the author's name and photo inside it.
useEffect(() => {
// helper function
const callIt = (unsub) => unsub();
// First onSnapshot
const tweetsNestedCancelListenerCallbacks = [];
const tweetsCancelListenerCallback = firestore
.collection('tweets')
.onSnapshot((tweetSnapshot) => {
const newTweets = [];
const expectedTweetCount = tweetSnapshot.docs.length;
// cancel nested subscriptions
tweetsNestedCancelListenerCallbacks.forEach(callIt);
// clear the array, but don't lose the reference
tweetsNestedCancelListenerCallbacks.length = 0;
tweetsNestedCancelListenerCallbacks.push(
...tweetSnapshot.docs
.map((tweetDoc) => { // (tweetDoc) => Unsubscribe
const tweetId = tweetDoc.id;
//Second onSnapshot
const userPrefNestedCancelListenerCallbacks = [];
const userPrefCancelListenerCallback = firestore
.collection('user_preferences')
.where('uid', '==', tweetDoc.data().uid)
.limitToFirst(1)
.onSnapshot((userPrefSnapshot) => {
const userPrefDoc = userPrefSnapshot.docs[0];
// cancel nested subscriptions
userPrefNestedCancelListenerCallbacks.forEach(callIt);
// clear the array, but don't lose the reference
userPrefNestedCancelListenerCallbacks.length = 0;
//Third onSnapshot
const userPhotoCancelListenerCallback = firestore
.collection('user_photos')
.where('id', '==', tweetDoc.data().uid)
.limitToFirst(1)
.onSnapshot((userPhotoSnapshot) => {
const userPhotoDoc = userPhotoSnapshot.docs[0];
// Taking the whole data I need from all snapshots
const newData = {
id: tweetId,
...tweetDoc.data(),
author: userPrefDoc.data().username,
authorColor: userPrefDoc.data().color,
authorPhoto: userPhotoDoc.data().photoURL,
};
const existingTweetObject = tweets.find(t => t.id === tweetId);
if (existingTweetObject) {
// merge in changes to existing tweet
Object.assign(existingTweetObject, newData);
if (expectedTweetCount === newTweets.length) {
setTweets([...newTweets]); // force rerender with new info
}
} else {
// fresh tweet
tweets.push(newData);
if (expectedTweetCount === newTweets.length) {
setTweets(newTweets); // trigger initial render
}
}
});
userPrefNestedCancelListenerCallbacks.push(userPhotoCancelListenerCallback);
});
// return an Unsubscribe callback for this listener and its nested listeners.
return () => {
userPrefCancelListenerCallback();
userPrefNestedCancelListenerCallbacks.forEach(callIt);
}
})
);
});
// return an Unsubscribe callback for this listener and its nested listeners.
return () => {
tweetsCancelListenerCallback();
tweetsNestedCancelListenerCallbacks.forEach(callIt);
};
}, []);
Edit: Splitting the code in two components
Note: Changed limitToFirst(1) --> limit(1). Splitting the fetch logic in two components simplified the onSnapshot approach!
1.The Parent Component
useEffect(() => {
const tweetsUnsubscribeCallback = firestore
.collection('tweets')
.onSnapshot((tweetSnapshot) => {
const mappedtweets = tweetSnapshot.docs.map((tweetDoc) => {
return {
id: tweetDoc.id,
...tweetDoc.data(),
};
});
setTweets(mappedtweets);
});
return () => tweetsUnsubscribeCallback();
}, []);
2.The Child Component: Tweet
useEffect(() => {
// Helper Function
const unSubscribe = (unsub) => unsub();
//------------getting the AUTHOR USER PREFERENCE
const userPrefNestedUnsubscribeCallbacks = [];
const userPrefUnsubscribeCallback = firestore
.collection('user_preferences')
.where('uid', '==', tweet.uid)
.limit(1)
.onSnapshot((userPrefSnapshot) => {
userPrefNestedUnsubscribeCallbacks.forEach(unSubscribe); // cancel nested subscriptions
userPrefNestedUnsubscribeCallbacks.length = 0; // clear the array, but don't lose the reference
//------------getting the AUTHOR PHOTO
const userPhotoUnsubscribeCallback = firestore
.collection('user_photos')
.where('id', '==', tweet.uid)
.limit(1)
.onSnapshot((userPhotoSnapshot) => {
// Taking the whole data I need from all snapshots
setAuthor({
author: userPrefSnapshot.docs[0].data().username,
authorColor: userPrefSnapshot.docs[0].data().color,
authorPhoto: userPhotoSnapshot.docs[0].data().photoURL,
});
});
userPrefNestedUnsubscribeCallbacks.push(userPhotoUnsubscribeCallback);
});
return () => {
userPrefUnsubscribeCallback();
userPrefNestedUnsubscribeCallbacks.forEach(unSubscribe);
};
}, []);
Basically, you've pushed the promises to your promise array in the state they were before you you processed their data. You want to make use of the Promise.all(docs.map((doc) => Promise<Result>)) pattern here where each document should return a single Promise containing its final result. This then means that the Promise.all will resolve with Result[].
Note: If inside a Promise you are mutating a variable outside of the Promise (e.g. pushing to an array), that is generally a sign that you are doing something wrong and you should rearrange your code.
Here's a quick example of throwing this together:
useEffect(() => {
let unsubscribed = false;
//------------getting the TWEETS with onSnapshot()-------------
const cancelSuscription = firestore
.collection('tweets')
.onSnapshot((snapshot) => {
const tweetsMappedPromises = snapshot.docs.map((doc) => {
let tweetAndAuthor;
const tweetMappped = {
text: doc.data().text,
likes: doc.data().likes,
email: doc.data().email,
created: doc.data().created,
uid: doc.data().uid,
id: doc.id,
};
//------------getting the AUTHOR USER PREFERENCES with .get()-------------
const userPreferencePromise = firestore
.collection('user_preferences')
.where('uid', '==', tweetMappped.uid)
.limitToFirst(1)
.get()
.then((prefDocQuerySnapshot) => {
const firstPrefDoc = photoDocQuerySnapshot.docs[0];
const { username, color } = firstPrefDoc.data();
return { username, color };
});
//------------getting the AUTHOR PHOTO with .get()-------------
const userPhotoPromise = firestore
.collection('user_photos')
.where('id', '==', tweetMappped.uid)
.limitToFirst(1)
.get()
.then((photoDocQuerySnapshot) => {
const firstPhotoDoc = photoDocQuerySnapshot.docs[0];
return firstPhotoDoc.get("photoURL");
});
//--------------------assemble this result---------------------
return Promises.all([userPreferencePromise, userPhotoPromise])
.then(([authorPreference, authorPhoto]) => {
return {
...tweetMappped,
author: authorPreference.username,
authorColor: authorPreference.color,
authorPhoto: authorPhoto.photoURL,
};
});
});
Promise.all(tweetsMappedPromises)
.then((tweetsMapped) => {
if (unsubscribed) return; // ignore result, dealing with out of date data
setTweets(tweetsMapped);
})
.catch((err) => {
if (unsubscribed) return; // ignore result, dealing with out of date data
// important! handle errors
});
});
return () => {
unsubscribed = true;
cancelSuscription();
}
}, []);
Notes:
You may benefit from using async/await syntax here instead.
On new onSnapshot calls, snapshot.docChanges() can be used to make it more efficient and speed up rerenders by only updating the entries that have changed (e.g. added/removed/modified). You would use setTweets(previousTweetsMapped => /* newTweetsMapped */) for this.

ReactJs Unable to setSate in componentDidMount from async function

I'm calling an async function (getData()) in componentDidMount, and I'm trying to use this.setState with result of that function.
componentDidMount() {
let newData = getData();
newPodData.then(function (result) {
console.log('result', result)
this.setState({result})
})
}
However, I'm having issues getting my state to properly update. Some additional context - I'm trying to set my initial state with data I am receiving from a database. Is my current approach correct? What's the best way to accomplish this? Here's my async function for more context:
const getTeamData = async () => {
const getTeamMembers = async () => {
let res = await teamMemberService.getTeamMembers().then(token => { return token });
return res;
}
const getActiveTeams = async () => {
let res = await teamService.getActiveTeams().then(token => { return token });
return res;
}
const teamMemberResult = await getTeamMembers()
const activeTeamsResult = await getActiveTeams();
// get team member data and add to teamMember object
let teamMemberData = teamMemberResult.reduce((acc, curr) => {
acc.teamMembers[curr.id] = curr;
return acc;
}, {
teamMembers: {}
});
// get team ids and add to teamOrder array
let activeTeamsData = activeTeamsResult.map(team => team.id)
let key = 'teamOrder'
let obj = []
obj[key] = activeTeamsData;
const newObject = Object.assign(teamMemberData, obj)
return newObject;
}
export default getTeamData;
Changing the function inside the then handler to an arrow function should fix it. e.g:
componentDidMount() {
let newData = getData();
newPodData.then((result) => {
console.log('result', result)
this.setState({result})
})
}
But I'll like to suggest a better way to write that.
async componentDidMount() {
let result = await getData();
this.setState({result})
}

Axios auto refresh every 60 seconds with Reactjs

I'm using ReactJS as a javascript library and I am getting the data in componentDidMount() using axios. These received data must be taken again every 60 seconds. What is the most efficient and good way to do this?
componentDidMount() {
const newGteeChartSeries = [];
const newGteeChartCategories = [];
const newmultiSelectOption = [];
axios.get(`http://www.xxxxxxx:xxxx/api/groupdata`).then(res => {
this.state.gteeChartSeries.map(() => {
const data = [];
res.data.map((item, index) => {
data.push(item.gtee);
newGteeChartCategories.push(item.isyeri);
newmultiSelectOption.push({ id: item.id, isyeri: item.isyeri });
});
newGteeChartSeries.push({ name: "GTEE", data });
});
this.setState({
teeTableData: res.data,
gteeChartSeries: newGteeChartSeries,
multiSelectOptions: newmultiSelectOption,
gteeChartoptions: {
...this.state.options,
xaxis: {
categories: newGteeChartCategories
}
}
});
});
}
One way of going about it is to move the data fetching logic to a separate method and create an interval that will invoke this method every 60 seconds.
Make sure you store the number returned from setInterval on the component instance so you can use clearInterval in componentWillUnmount.
Example
class MyComponent extends React.Component {
interval = null;
componentDidMount() {
this.interval = setInterval(this.getData, 60000);
this.getData();
}
componentWillUnmount() {
clearInterval(this.interval);
}
getData = () => {
const newGteeChartSeries = [];
const newGteeChartCategories = [];
const newmultiSelectOption = [];
axios.get(`http://www.xxxxxxx:xxxx/api/groupdata`).then(res => {
this.state.gteeChartSeries.forEach(() => {
const data = [];
res.data.forEach((item, index) => {
data.push(item.gtee);
newGteeChartCategories.push(item.isyeri);
newmultiSelectOption.push({ id: item.id, isyeri: item.isyeri });
});
newGteeChartSeries.push({ name: "GTEE", data });
});
this.setState({
teeTableData: res.data,
gteeChartSeries: newGteeChartSeries,
multiSelectOptions: newmultiSelectOption,
gteeChartoptions: {
...this.state.options,
xaxis: {
categories: newGteeChartCategories
}
}
});
});
};
}
I would suggest abstracting the api request into its own function
componentDidMount(){
setInterval(yourApiCallFn(),60000)
}
You can wrap all in a function.
Call that function in ComponentDidMount(), and use setInterval(myFunction(), 60000) to call that function every 60 seconds
Below works without syntax error.Call that function without parenthesis
``componentDidMount() {
    this.interval = setInterval( this.props.Details, 6000);
    this.props.Details()
  }
  componentWillUnmount() {
    clearInterval(this.interval);
  }``
Well let's do that with a normal javascript setTimeInterval.
let intervalLoop = null; // a class variable
componentDidMount() {
const newGteeChartSeries = [];
const newGteeChartCategories = [];
const newmultiSelectOption = [];
this.intervalLoop = setInterval(()=>{
axios.get(`http://www.xxxxxxx:xxxx/api/groupdata`).then(res => {
this.state.gteeChartSeries.map(() => {
const data = [];
res.data.map((item, index) => {
data.push(item.gtee);
newGteeChartCategories.push(item.isyeri);
newmultiSelectOption.push({
id: item.id,
isyeri: item.isyeri
});
});
newGteeChartSeries.push({
name: "GTEE",
data
});
});
this.setState({
teeTableData: res.data,
gteeChartSeries: newGteeChartSeries,
multiSelectOptions: newmultiSelectOption,
gteeChartoptions: {
...this.state.options,
xaxis: {
categories: newGteeChartCategories
}
}
});
});
}, 60000);
}
// need to cleanup the timeinterval whenever we destroy the component
componentWillUnmount(){
clearInterval(this.intervalLoop)
}

MobX don't update react DOM in fetch promise callback

I am trying to update a react dom by changing an observable mobx variable inside a fetch callback in a react typescript app but mobx don't show any reaction on variable change.
I define my variable like this:
#observable data:any = []
and in my constructor i change data value:
constructor(){
this.data.push(
{
count:0,
dateTime:'2017'
})
this.getData();
}
its work fine and update dom properly as expected.
in getData() method i write a fetch to retrive data from server :
#action getData(){
this.data.push(
{
count:1,
dateTime:'2018'
})
fetch(request).then(response=>response.json())
.then(action((data:Array<Object>)=>{
this.data.push(data)
console.log(data)
}));
}
so my view now shows 2 value the 2017 and 2018 object data but the 2019 data that I get from the server is not showing. the log shows the correct values and variable filled in a right way but mobx don't update view after I set any variable in fetch function callback and I don't know why?
p.s: I do the same in ECMA and there was no problem but in typescript mobx act differently
Check my approach:
import { action, observable, runInAction } from 'mobx'
class DataStore {
#observable data = null
#observable error = false
#observable fetchInterval = null
#observable loading = false
//*Make request to API
#action.bound
fetchInitData() {
const response = fetch('https://poloniex.com/public?command=returnTicker')
return response
}
//*Parse data from API
#action.bound
jsonData(data) {
const res = data.json()
return res
}
//*Get objects key and push it to every object
#action.bound
mapObjects(obj) {
const res = Object.keys(obj).map(key => {
let newData = obj[key]
newData.key = key
return newData
})
return res
}
//*Main bound function that wrap all fetch flow function
#action.bound
async fetchData() {
try {
runInAction(() => {
this.error = false
this.loading = true
})
const response = await this.fetchInitData()
const json = await this.jsonData(response)
const map = await this.mapObjects(json)
const run = await runInAction(() => {
this.loading = false
this.data = map
})
} catch (err) {
console.log(err)
runInAction(() => {
this.loading = false
this.error = err
})
}
}
//*Call reset of MobX state
#action.bound
resetState() {
runInAction(() => {
this.data = null
this.fetchInterval = null
this.error = false
this.loading = true
})
}
//*Call main fetch function with repeat every 5 seconds
//*when the component is mounting
#action.bound
initInterval() {
if (!this.fetchInterval) {
this.fetchData()
this.fetchInterval = setInterval(() => this.fetchData(), 5000)
}
}
//*Call reset time interval & state
//*when the component is unmounting
#action.bound
resetInterval() {
if (this.fetchInterval) {
clearTimeout(this.fetchInterval)
this.resetState()
}
}
}
const store = new DataStore()
export default store
as #mweststrate mentioned in the comments, it was an observer problem and when I add #observer on top of my react class the problem get fixed

React native not waiting for response from API before continuing

I have just started playing about with react native and I have a problem that functions aren't waiting for responses before continuing.
So in Chrome my console log displays:
userStore
this state contents
returned data from api / userstore [object Object]
Basically getUserDetails is executed and in that time while the api is being called the setData function runs, and it completes before the api result has been returned.
I would like the getUserDetails functio to complete before setData is called.
I have had a look at resources online, but am at a loss. The code I am using is below (This has been stripped down for ease of reading nb. I am using mobx)
UserScreen.js
constructor (props) {
super(props);
this.state = {
data: null
};
}
async componentDidMount() {
this.props.commonStore.setLoading(true);
await this.props.userStore.getUserDetails('1');
this.setData();
this.props.commonStore.setLoading(false);
}
setData() {
this.setState({
userDetails: this.props.userStore.userDetails
});
console.log('userStore' + this.props.userStore.userDetails)
console.log('this state contents '+ this.state.userDetails);
}
render () {
if(this.props.commonStore.isLoading===false) {
return (<View><Text>Ready!!</Text></View>)
}else{}
return (<View><Text>Loading</Text></View>)
}
}
UserStore.js
#action getUserDetails = (userID) => {
axios.get('http://192.168.1.9/user/' + userID)
.then(response => {
console.log('returned data from api / userstore ' +response.data.user);
this.userdetails = response.data.user;
}).catch(error => {
console.log(error);
this.error = error
}) }
Thanks
If you have stumbled upon the beauty of Mobx, you need to move towards a stateless solution i.e.:
UserScreen.js
componentDidMount() {
this.getUserDetails();
}
async getUserDetails(){
await this.props.UserStore.getUserDetails('1');
}
render () {
const { isLoading, userDetails, error } = this.props.UserStore
return (<View>
{(!!isLoading)?<Text>{userDetails}</Text>:<Text>Loading</Text>}
</View>)
}
UserStore.js
#observable userdetails = {};
#observable isLoading = false;
#observable error = {};
async getUserDetails(userID) {
this.isLoading = true;
try {
await axios.get('http://192.168.1.9/user/' + userID)
.then(response => {
console.log('returned data from api / userstore '+response.data.user);
this.userdetails = response.data.user;
this.isLoading = false;
})
.catch(error => {
console.log(error);
this.error = error
})
} catch (e) {
console.log('ERROR', e);
this.isLoading = false;
}
}
As you are passing the data into an observable array i.e. #observable userdetails = {}; Mobx will automatically update the state, once the promise / await is complete.
P.S. Mobx rules OK!! :o)

Resources