I'm using firebase/firestore and I'm looking a way to return promise of snapshot.
onlineUsers(){
// i want to return onSnapshot
return this.status_database_ref.where('state','==','online').onSnapshot();
}
in other file I did
componentDidMount(){
// this.unsubscribe = this.ref.where('state','==','online').onSnapshot(this.onCollectionUpdate)
firebaseService.onlineUsers().then(e=>{
console.log(e)
})
}
I get the errors
Error: Query.onSnapshot failed: Called with invalid arguments.
TypeError: _firebaseService2.default.unsubscribe is not a function
if i do this way
onlineUsers(){
return this.status_database_ref.where('state','==','online').onSnapshot((querySnapshot)=>{
return querySnapshot
})
}
I get
TypeError: _firebaseService2.default.onlineUsers(...).then is not a function
in addition,
when I do this way
this.unsubscribe = firebaseService.onlineUsers().then((querySnapshot)=>{
console.log(querySnapshot.size)
this.setState({count:querySnapshot.size})
})
// other file
onlineUsers(callback) {
return this.status_database_ref.where('state', '==', 'online').get()
}
it not listen to change into firebase, means if I change in firebase it's not update or change the size..
---- firestore function ---
I tried to make firestore function that trigger each time the UserStatus node updated but this take some seconds and it slow for me.
module.exports.onUserStatusChanged = functions.database
.ref('/UserStatus/{uid}').onUpdate((change, context) => {
// Get the data written to Realtime Database
const eventStatus = change.after.val();
// Then use other event data to create a reference to the
// corresponding Firestore document.
const userStatusFirestoreRef = firestore.doc(`UserStatus/${context.params.uid}`);
// It is likely that the Realtime Database change that triggered
// this event has already been overwritten by a fast change in
// online / offline status, so we'll re-read the current data
// and compare the timestamps.
return change.after.ref.once("value").then((statusSnapshot) => {
return statusSnapshot.val();
}).then((status) => {
console.log(status, eventStatus);
// If the current timestamp for this data is newer than
// the data that triggered this event, we exit this function.
if (status.last_changed > eventStatus.last_changed) return status;
// Otherwise, we convert the last_changed field to a Date
eventStatus.last_changed = new Date(eventStatus.last_changed);
// ... and write it to Firestore.
//return userStatusFirestoreRef.set(eventStatus);
return userStatusFirestoreRef.update(eventStatus);
});
});
function to calculate and update count of online users
module.exports.countOnlineUsers = functions.firestore.document('/UserStatus/{uid}').onWrite((change, context) => {
const userOnlineCounterRef = firestore.doc('Counters/onlineUsersCounter');
const docRef = firestore.collection('UserStatus').where('state', '==', 'online').get().then(e => {
let count = e.size;
return userOnlineCounterRef.update({ count })
})
})
A Promise in JavaScript can resolve (or reject) exactly once. A onSnapshot on the other hand can give results multiple times. That's why onSnapshot doesn't return a promise.
In your current code, you're left with a dangling listener to status_database_ref. Since you don't do anything with the data, it is wasteful to keep listening for it.
Instead of using onSnapshot, use get:
onlineUsers(callback){
this.status_database_ref.where('state','==','online').get((querySnapshot)=>{
callback(querySnapshot.size)
})
}
Or in your original approach:
onlineUsers(){
return this.status_database_ref.where('state','==','online').get();
}
I know it's too late but here is my solution using TypeScript & Javascript.
TYPESCRIPT
const _db=firebase.firestore;
const _collectionName="users";
onDocumentChange = (
document: string,
callbackSuccess: (currentData: firebase.firestore.DocumentData, source?: string | 'Local' | 'Server') => void,
callbackError?: (e: Error) => void,
callbackCompletion?: () => void
) => {
this._db.collection(this._collectionName).doc(document).onSnapshot(
{
// Listen for document metadata changes
includeMetadataChanges: true
},
(doc) => {
const source = doc.metadata.hasPendingWrites ? 'Local' : 'Server';
callbackSuccess(doc.data(), source);
},
(error) => callbackError(error),
() => callbackCompletion()
);
};
JAVASCRIPT (ES5)
var _this = this;
onDocumentChange = function (document, callbackSuccess, callbackError, callbackCompletion) {
_this._db.collection(_this._collectionName).doc(document).onSnapshot({
// Listen for document metadata changes
includeMetadataChanges: true
}, function (doc) {
var source = doc.metadata.hasPendingWrites ? 'Local' : 'Server';
callbackSuccess(doc.data(), source);
}, function (error) { return callbackError(error); }, function () { return callbackCompletion(); });
};
I found a way to do that
onlineUsers(callback){
return this.status_database_ref.where('state','==','online').onSnapshot((querySnapshot)=>{
callback(querySnapshot.size)
})
}
componentDidMount(){
this.unsubscribe = firebaseService.onlineUsers(this.onUpdateOnlineUsers);
console.log(this.unsubscribe)
}
onUpdateOnlineUsers(count){
this.setState({count})
}
componentWillUnmount(){
this.unsubscribe();
}
Related
I am trying to get data from local memory using asyncStorage but there is one issue
useEffect( async () => {
try {
if(activemanagegroup !== null) {
var groupValue = JSON.stringify(activemanagegroup)
await AsyncStorage.setItem('managementGroup', groupValue)
}
var listValue = JSON.stringify(list)
await AsyncStorage.setItem('selectedList', listValue)
} catch (e) {
console.log('Failed to save data')
}
},[activemanagegroup, list])
useEffect(() => {
async function getData() {
try {
const managementGroupValue = await AsyncStorage.getItem('managementGroup')
const managedUsersList = await AsyncStorage.getItem('selectedList')
const activeManagementGroupSelected = managementGroupValue != null ? JSON.parse(managementGroupValue) : null
const activeList = managedUsersList != null ? JSON.parse(managedUsersList) : null
setActiveManagementGroup(activeManagementGroupSelected)
setNewList(activeList)
} catch (error) {
console.log('error getting data', error)
}
}
getData()
},[activemanagegroup])
the problem is selectedList updates a second later after managementGroup and due to that I end up getting old selectedList. How I can delay the call and make sure I get updated selectedList ?
Note: I am storing both these values once user presses a button.
I wouldn't recommend using AsyncStorage to retrieve the data more than once; once you have the data loaded initially, you should use React's built-in state management solutions to store the data instead of re-reading it from AsyncStorage.
Thus, I'd move your getItem calls to a separate useEffect that only runs once and updates the local React state:
const [managementGroupValue, setManagementGroupValue] = useState(null)
const [managedUsersList, setManagedUsersList] = useState(null)
useEffect(() => {
async function getData() {
try {
setManagementGroupValue(await AsyncStorage.getItem('managementGroup'))
setManagedUsersList(await AsyncStorage.getItem('selectedList'))
} catch (error) {
console.log('error getting data', error)
}
}
getData()
}, [])
and then use the managementGroupValue and managedUsersList variables to refer to that data instead of retrieving it from AsyncStorage each time.
I am having a bit of trouble setting this up.
I have a folder that deals with all the Db API, so that concerns are separated.
I have one function that opens a connection and gets realtime updates whenever a value in the Db changes (Firebase Firestore).
I call this "listener" function once and would like to keep receiving the real time values within the function that invokes the "listener" function.
Any ideas how I could achieve this?
This is my code:
// LISTENER FN
export const getConnectionRequestsFromDb = () => {
const uid = getUID()
const q = query(
collection(database, "inbox"),
where("uids", "array-contains-any", [uid]),
where("type", "==", "connectionRequest"),
limit(50)
)
const data = []
const unsubscribe = onSnapshot(q, (querySnapshot) => {
// Initially return an empty array, milliseconds later the actual values
querySnapshot.forEach((doc) => data.push(doc.data()))
})
const formattedData = convertDatesIntoJsTimestamps(data)
return [formattedData, null, unsubscribe]
}
// INVOKING FN
export const getConnectionRequests = () => {
return async (dispatch) => {
dispatch({ type: "CONNECTIONS_ACTIONS/GET_CONNECTIONS_REQUEST/pending" })
// I want to keep listening for realtime updates here and dispatch payloads accordingly
const [data, error, unsubscribe] = getConnectionRequestsFromDb()
if (data) {
return dispatch({
type: "CONNECTIONS_ACTIONS/GET_CONNECTIONS_REQUEST/fulfilled",
payload: data,
})
}
}
}
There is an SSE endpoint that shares a subscription if the consumer with the same key is already subscribed. If there is an active subscription the data is being polled from another client.
The problem is that the outer subscription never seems to catch the error and delegate it to the router in order to close the connection with the client: polling stops, but connection stays active.
I think the issue is how I start the subscription that is to be shared... but I can't think of a way to resolve this in another way currently.
Router (SSE) / outer subscription:
...
const clientId = Date.now();
const newClient = {
id: clientId,
res,
};
clients.push(newClient);
const sub = subscriptionService.listenToChanges(req.context, categoryIds).subscribe({
next: (data) => {
if (JSON.stringify(data) !== '{}') {
newClient.res.write(`data: ${JSON.stringify(data)}\n\n`);
} else {
newClient.res.write(': poke...\n\n');
}
},
error: () => {
// we never get here...
next(new InternalError());
clients = clients.filter((c) => c.id !== clientId);
res.end();
},
complete: () => {
res.end();
clients = clients.filter((c) => c.id !== clientId);
},
});
req.on('close', () => {
subscriptionService.stopListening(req.context);
sub.unsubscribe();
clients = clients.filter((c) => c.id !== clientId);
});
...
SubscriptionService
...
#trace()
public listenToChanges(ctx: Context, ids: string[]): Observable<{ [key: string]: Data }> {
const key = ctx.user?.email || ClientTypeKey.Anon;
try {
if (this.pool$[key]) {
return this.pool$[key];
}
this.poolSource[key] = new BehaviorSubject<{ [p: string]: Data }>({});
this.pool$[key] = this.poolSource[key].asObservable();
this.fetchData(ctx, ids);
return this.pool$[key].pipe(
catchError((e) => throwError(e)), // we never get here...
);
} catch (e) {
throw new Error(`Subscription Service Listen returned an error: "${e}"`);
}
}
...
private fetchData(ctx: Context, ids: string[]): void {
const key = ctx.user?.email || ClientTypeKey.Anon;
const sub = this.service.getData(ctx, ids)
.pipe(
catchError((e) => throwError(e)),
).subscribe(
(r) => this.poolSource[key].next(r),
(e) => throwError(e), // last time the error is caught
);
this.subscriptions[key] = sub;
}
...
Polling Service
...
#trace()
public getData(ctx: Context, ids: string[]): Observable<{[key: string]: Data}> {
try {
const key = ctx.user?.email || ClientTypeKey.Anon;
const pollingInterval = config.get('services.pollingInterval') || 10000;
return interval(pollingInterval).pipe(
startWith(0),
switchMap(() => this.getConfig(ctx, !!this.cachedData[key])),
map((r) => this.getUpdatedData(ctx, r.data, ids)),
catchError((e) => throwError(e)),
);
} catch (e) {
throw new Error(`Get Data returned an error: "${e}"`);
}
}
...
throwError doesn't actually throw an error, but rather creates an observable that emits an error.
From the docs:
[throwError] Creates an observable that will create an error instance and push it to the consumer as an error immediately upon subscription.
This is why using it inside subscribe does not work as intended. You should simply throw:
.subscribe(
(r) => this.poolSource[key].next(r),
(e) => throw new Error(e)
);
It seems like you have some unnecessary complexity in the way you are calling fetchData() in order to subscribe and push the result into a BehaviorSubject. I don't know all your requirements, but it seems like maybe you don't need the BehaviorSubject at all.
Instead of subscribing in fetchData(), you could simply return the observable and add that into your pool$ array, or maybe even get rid of fetchData() altogether:
public listenToChanges(ctx: Context, ids: string[]): Observable<{ [key: string]: Data }> {
const key = ctx.user?.email || ClientTypeKey.Anon;
try {
if (!this.pool$[key]) {
this.pool$[key] = this.service.getData(ctx, ids).pipe(
catchError((e) => throwError(e))
);
}
return this.pool$[key];
} catch (e) {
throw new Error(`Subscription Service Listen returned an error: "${e}"`);
}
}
Notes:
with the above simplification, maybe you no longer need the outer try/catch
this isn't a complete solution and may require some tweaks in other places of your code. I just wanted to point out, what seems like unnecessary complexity.
How can I build a function which gets some data asynchronously then uses that data to get more asynchronous data?
I am using Dexie.js (indexedDB wrapper) to store data about a direct message. One thing I store in the object is the user id which I'm going to be sending messages to. To build a better UI I'm also getting some information about that user such as the profile picture, username, and display name which is stored on a remote rdbms. To build a complete link component in need data from both databases (local indexedDB and remote rdbms).
My solution returns an empty array. It is being computed when logging it in Google Chrome and I do see my data. However because this is not being computed at render time the array is always empty and therefor I can't iterate over it to build a component.
const [conversations, setConversations] = useState<IConversation[]>()
const [receivers, setReceivers] = useState<Profile[]>()
useEffect(() => {
messagesDatabase.conversations.toArray().then(result => {
setConversations(result)
})
}, [])
useEffect(() => {
if (conversations) {
const getReceivers = async () => {
let receivers: Profile[] = []
await conversations.forEach(async (element) => {
const receiver = await getProfileById(element.conversationWith, token)
// the above await is a javascript fetch call to my backend that returns json about the user values I mentioned
receivers.push(receiver)
})
return receivers
}
getReceivers().then(receivers => {
setReceivers(receivers)
})
}
}, [conversations])
/*
The below log logs an array with a length of 0; receivers.length -> 0
but when clicking the log in Chrome I see:
[
0: {
avatarURL: "https://lh3.googleusercontent.com/..."
displayName: "Cool guy"
userId: "1234"
username: "cool_guy"
}
1: ...
]
*/
console.log(receivers)
My plan is to then iterate over this array using map
{
receivers && conversations
? receivers.map((element, index) => {
return <ChatLink
path={conversations[index].path}
lastMessage={conversations[index].last_message}
displayName={element.displayName}
username={element.username}
avatarURL={element.avatarURL}
key={index}
/>
})
: null
}
How can I write this to not return a empty array?
Here's a SO question related to what I'm experiencing here
I believe your issue is related to you second useEffect hook when you attempt to do the following:
const getReceivers = async () => {
let receivers: Profile[] = []
await conversations.forEach(async (element) => {
const receiver = await getProfileById(element.conversationWith, token)
receivers.push(receiver)
})
return receivers
}
getReceivers().then(receivers => {
setReceivers(receivers)
})
}
Unfortunately, this won't work because async/await doesn't work with forEach. You either need to use for...of or Promise.all() to properly iterate through all conversations, call your API, and then set the state once it's all done.
Here's is a solution using Promise.all():
function App() {
const [conversations, setConversations] = useState<IConversation[]>([]);
const [receivers, setReceivers] = useState<Profile[]>([]);
useEffect(() => {
messagesDatabase.conversations.toArray().then(result => {
setConversations(result);
});
}, []);
useEffect(() => {
if (conversations.length === 0) {
return;
}
async function getReceivers() {
const receivers: Profile[] = await Promise.all(
conversations.map(conversation =>
getProfileById(element.conversationWith, token)
)
);
setReceivers(receivers);
}
getReceivers()
}, [conversations]);
// NOTE: You don't have to do the `receivers && conversations`
// check, and since both are arrays, you should check whether
// `receivers.length !== 0` and `conversations.length !== 0`
// if you want to render something conditionally, but since your
// initial `receivers` state is an empty array, you could just
// render that instead and you won't be seeing anything until
// that array is populated with some data after all fetching is
// done, however, for a better UX, you should probably indicate
// that things are loading and show something rather than returning
// an empty array or null
return receivers.map((receiver, idx) => <ChatLink />)
// or, alternatively
return receivers.length !== 0 ? (
receivers.map((receiver, idx) => <ChatLink />)
) : (
<p>Loading...</p>
);
}
Alternatively, using for...of, you could do the following:
function App() {
const [conversations, setConversations] = useState<IConversation[]>([]);
const [receivers, setReceivers] = useState<Profile[]>([]);
useEffect(() => {
messagesDatabase.conversations.toArray().then(result => {
setConversations(result);
});
}, []);
useEffect(() => {
if (conversations.length === 0) {
return;
}
async function getReceivers() {
let receivers: Profile[] = [];
const profiles = conversations.map(conversation =>
getProfileById(conversation.conversationWith, token)
);
for (const profile of profiles) {
const receiver = await profile;
receivers.push(receiver);
}
return receivers;
}
getReceivers().then(receivers => {
setReceivers(receivers);
});
}, [conversations]);
return receivers.map((receiver, idx) => <ChatLink />);
}
i think it is happening because for getReceivers() function is asynchronous. it waits for the response, in that meantime your state renders with empty array.
you can display spinner untill the response received.
like
const[isLoading,setLoading]= useState(true)
useEffect(()=>{
getReceivers().then(()=>{setLoading(false)}).catch(..)
} )
return {isLoading ? <spinner/> : <yourdata/>}
Please set receivers initial value as array
const [receivers, setReceivers] = useState<Profile[]>([])
Also foreach will not wait as you expect use for loop instead of foreach
I am not sure it is solution for your question
but it could help you to solve your error
I have a function that refreshes the data of my component when the function is called. At this moment it only works for one component at a time. But I want to refresh two components at once. This is my refresh function:
fetchDataByName = name => {
const { retrievedData } = this.state;
const { fetcher } = this.props;
const fetch = _.find(fetcher, { name });
if (typeof fetch === "undefined") {
throw new Error(`Fetch with ${name} cannot be found in fetcher`);
}
this.fetchData(fetch, (error, data) => {
retrievedData[name] = data;
this._isMounted && this.setState({ retrievedData });
});
};
My function is called like this:
refresh("meetingTypes");
As it it passed as props to my component:
return (
<Component
{...retrievedData}
{...componentProps}
refresh={this.fetchDataByName}
/>
);
I tried passing multiple component names as an array like this:
const args = ['meetingTypes', 'exampleMeetingTypes'];
refresh(args);
And then check in my fetchDataByName function if name is an array and loop through the array to fetch the data. But then the function is still executed after each other instead of at the same time. So my question is:
What would be the best way to implement this that it seems like the
function is executed at once instead of first refreshing meetingTypes
and then exampleMeetingTypes?
Should I use async/await or are there better options?
The fetchData function:
fetchData = (fetch, callback) => {
const { componentProps } = this.props;
let { route, params = [] } = fetch;
let fetchData = true;
// if fetcher url contains params and the param can be found
// in the component props, they should be replaced.
_.each(params, param => {
if (componentProps[param]) {
route = route.replace(`:${param}`, componentProps[param]);
} else {
fetchData = false; // don't fetch data for this entry as the params are not given
}
});
if (fetchData) {
axios
.get(route)
.then(({ data }) => {
if (this.isMounted) {
callback(null, data);
}
})
.catch(error => {
if (error.response.status == 403) {
this._isMounted && this.setState({ errorCode: 403 });
setMessage({
text: "Unauthorized",
type: "error"
});
}
if (error.response.status == 401) {
this._isMounted && this.setState({ errorCode: 401 });
window.location.href = "/login";
}
if (error.response.status != 403) {
console.error("Your backend is failing.", error);
}
callback(error, null);
});
} else {
callback(null, null);
}
};
I assume fetchData works asynchronously (ajax or similar). To refresh two aspects of the data in parallel, simply make two calls instead of one:
refresh("meetingTypes");
refresh("exampleMeetingTypes");
The two ajax calls or whatever will run in parallel, each updating the component when it finishes. But: See the "Side Note" below, there's a problem with fetchDataByName.
If you want to avoid updating the component twice, you'll have to update fetchDataByName to either accept multiple names or to return a promise of the result (or similar) rather than updating the component directly, so the caller can do multiple calls and wait for both results before doing the update.
Side note: This aspect of fetchDataByName looks suspect:
fetchDataByName = name => {
const { retrievedData } = this.state; // <=============================
const { fetcher } = this.props;
const fetch = _.find(fetcher, { name });
if (typeof fetch === "undefined") {
throw new Error(`Fetch with ${name} cannot be found in fetcher`);
}
this.fetchData(fetch, (error, data) => {
retrievedData[name] = data; // <=============================
this._isMounted && this.setState({ retrievedData });
});
};
Two problems with that:
It updates an object stored in your state directly, which is something you must never do with React.
It replaces the entire retrievedData object with one that may well be stale.
Instead:
fetchDataByName = name => {
// *** No `retrievedData` here
const { fetcher } = this.props;
const fetch = _.find(fetcher, { name });
if (typeof fetch === "undefined") {
throw new Error(`Fetch with ${name} cannot be found in fetcher`);
}
this.fetchData(fetch, (error, data) => {
if (this._isMounted) { // ***
this.setState(({retrievedData}) => ( // ***
{ retrievedData: {...retrievedData, [name]: data} } // ***
); // ***
} // ***
});
};
That removes the in-place mutation of the object with spread, and uses an up-to-date version of retrievedData by using the callback version of setState.