Return value firestore onSnapshot in react - javascript

i have a onSnapshot query in a function:
//firebaseutil.js
export async function getShorts(uid) {
const q = query(collection(db, 'shorted'), where('owner', '==', uid));
const unsubscribe = onSnapshot(q, (querySnapshot) => {
const urls = [];
querySnapshot.forEach((doc) => {
urls.push({
url: doc.data().url,
shorturl: doc.data().shorturl,
hits: doc.data().hits,
});
});
console.log(urls);
return urls;
});
}
Which correctly logs the data, and relog it if i change it on the firestore collection (as expected)
i am trying to access these data from a user dashboard this way:
//dashboard.js
import { getShorts } from '../lib/fbutils';
import { useEffect, useState } from 'react';
export default function Dashboard() {
const [myurls, setUrls] = useState([]);
useEffect(() => {
const fetchShorts = async () => {
if (user) {
const urls = await getShorts(user.uid);
setUrls(urls);
console.log(myurls);
console.log(urls);
}
};
fetchShorts();
}, []);
user.id is correctly set, but both urls and myurls are logged as undefined (i was thinking at least for a Promise pending)
what am i doing wrong? i usually use this pattern to retrieve data, but it's my first time i get data from a firestore subscription

The onSnapshot() does not return a promise and your getShorts() function returns before the data is received. You can return a promise from that function as shown below:
let fetched = false;
export function getShorts(uid) {
return new Promise((resolve, reject) => {
const q = query(collection(db, 'shorted'), where('owner', '==', uid));
const unsubscribe = onSnapshot(q, (querySnapshot) => {
const urls = querySnapstho.docs.map((d) => ({ id: d.id, ...d.data() }))
if (!fetched) {
// URLs fetched for first time, return value
fetched = true;
resolve(urls);
} else {
// URLs fetched already, an update received.
// TODO: Update in state directly
}
})
})
}
This should return all the URLs when you call the function await getShorts(user.uid); but for the updates received later, you'll have to update them in the state directly because the promise has resolved now.

Related

How to catch Firebase promise in React?

I have a simple function that checks if the user has Premium access or not:
export const checkPremium = async () =>{
if (auth.currentUser) {
const q = query(collection(db_firestore, 'users'));
onSnapshot(q, (querySnapshot) => {
querySnapshot.forEach((doc) => {
if (doc.id === auth.currentUser.uid) {
return doc.data().userSettings.hasPremium
}
});
})
}
else{
return false
}
}
I tried to catch this in various ways, but no luck, it always returns an "undefined" object.
const getPremium = async => {
checkPremium.then((response) => console.log(response))
}
const getPremium = async => {
let hasPremium = await checkPremium()
}
let hasPremium = checkPremium()
What is the correct way to get the returned Boolean value?
onSnapshot is meant for listening to a collection continuously, getting repeatedly notified as its value changes. It does not create a promise, so the promise returned by getPremium is unrelated to the data you will eventually get in onSnapshot. If you just want to get the value once, you should use getDocs:
export const checkPremium = async () =>{
if (auth.currentUser) {
const q = query(collection(db_firestore, 'users'));
const querySnapshot = await getDocs(q);
const match = querySnapshot.docs.find(doc => doc.id === auth.currentUser.uid);
if (match) {
return doc.data().userSettings.hasPremium);
} else {
return false;
}
}
else{
return false
}
}
Also, instead of getting all the users and then using client side code to find the one with the right id, you could just fetch that individual doc directly:
const ref = doc(db_firestore, 'users', auth.currentUser.uid)
const snapshot = await getDoc(ref);
const data = snapshot.data();
if (data) {
return data.userSettings.hasPremium
} else {
return false
}

React Native memory leak error after updating to Firebase version 9 onValue

I'm going through to update my code for the Firebase version 9 modular form. I am now using onValue. From what I'm reading it returns a function that removes the listener. But I'm still not doing it right because although it functions well at first, when I change the database on the backend with the app open I get the "can't perform a react state update on an unmounted component" error when I'm in a different app screen. See old and new code below please.
OLD CODE:
useEffect(() => {
loadListings();
},[]);
const loadListings = async () => {
setLoading(true);
updateInput('');
let testData = [];
let searchData = [];
db.ref('deals').once('value', (snapshot) =>{
snapshot.forEach((child)=>{
testData.push({
id: child.key,
title: child.val().hasOwnProperty('title') ? child.val().title : 'NA',
})
searchData.push(
child.val().title
)
})
})
.then(()=>{
checkMessages(testData);
setLoading(false);
})
.catch((error) => Sentry.Native.captureException('Error MessagesScreen function loadListings 1 ' + error));
}
NEW CODE:
useEffect(() => {
loadListings();
},[]);
const loadListings = async () => {
setLoading(true);
updateInput('');
const dbRef = ref(db, 'deals');
return onValue(dbRef , (snapshot) => {
let testData = [];
let searchData = [];
let storeData = filterStores;
snapshot.forEach((childSnapshot)=>{
testData.push({
id: childSnapshot.key,
title: childSnapshot.val().hasOwnProperty('title') ? childSnapshot.val().title : 'NA',
})
})
checkMessages(testData);
setLoading(false);
})
}
After receiving answer below I changed the useEffect to this instead and now it works:
useFocusEffect(
React.useCallback( () => {
async function fetchData() {
// You can await here
const response = await loadListings();
// ...
return () => response();
}
fetchData();
}, [])
);
You mentioned the unsubscribe function returned from onValue. In order to call it, I think you'll want to grab it from the invocation and then call it on some navigation state change.
Assuming you're using React Navigation, it might look something like this (using the useFocusEffect
import { useFocusEffect } from '#react-navigation/native';
function YourComponent() {
const [loading, setLoading] = React.useState(false)
useFocusEffect(
React.useCallback(async () => {
const unsubscribe = await loadListings();
return () => unsubscribe();
}, [])
);
const loadListings = async () => {
setLoading(true);
updateInput('');
const dbRef = ref(db, 'deals');
return onValue(dbRef , (snapshot) => {
let testData = [];
let searchData = [];
let storeData = filterStores;
snapshot.forEach((childSnapshot)=>{
testData.push({
id: childSnapshot.key,
title: childSnapshot.val().hasOwnProperty('title') ? childSnapshot.val().title : 'NA',
})
})
checkMessages(testData);
setLoading(false);
})
}
return <View />;
}
Also don't forget to either use async/await for your asynchronous loadListings function, or use .then(). Otherwise you'll be working with an unresolved promise.
I also found a related StackOverflow question that helped me get to this answer. Maybe that'll be of some use to you.

useState set call not reflecting change immediately prior to first render of app

New to React Hooks and unsure how to solve. I have the following snippet of code within my App.js file below.
What I am basically trying to achieve is to get the user logged in by calling the getUser() function and once I have the user id, then check if they are an authorised user by calling the function checkUserAccess() for user id.
Based on results within the the validIds array, I check to see if it's true or false and set authorised state to true or false via the setAuthorised() call.
My problem is, I need this to process first prior to performing my first render within my App.js file.
At the moment, it's saying that I'm not authroised even though I am.
Can anyone pls assist with what I am doing wrong as I need to ensure that authorised useState is set correctly prior to first component render of application, i.e. path="/"
const [theId, setTheId] = useState('');
const [authorised, setAuthorised] = useState(false);
const checkUserAccess = async (empid) => {
try {
const response = await fetch("http://localhost:4200/get-valid-users");
const allUsers = await response.json();
const validIds = allUsers.map(({ id }) => id);
const isAuthorised = validIds.includes(empid);
if (isAuthorised) {
setAuthorised(true)
} else {
setAuthorised(false)
}
} catch (err) {
console.error(err.message);
}
}
const getUser = async () => {
try {
const response = await fetch("http://localhost:4200/get-user");
const theId= await response.json();
setTheId(theId);
checkUserAccess(theId);
} catch (err) {
console.error(err.message);
}
}
useEffect(() => {
getUser();
}, []);
Unless you are wanting to partially render when you get the user ID, and then get the access level. There is no reason to have multiple useState's / useEffect's.
Just get your user and then get your access level and use that.
Below is an example.
const [user, setUser] = useState(null);
const checkUserAccess = async (empid) => {
const response = await fetch("http://localhost:4200/get-valid-users");
const allUsers = await response.json();
const validIds = allUsers.map(({ id }) => id);
const isAuthorised = validIds.includes(empid);
return isAuthorised;
}
const getUser = async () => {
try {
const response = await fetch("http://localhost:4200/get-user");
const theId= await response.json();
const access = await checkUserAccess(theId);
setUser({
theId,
access
});
} catch (err) {
console.error(err.message);
}
}
useEffect(() => {
getUser();
}, []);
if (!user) return <div>Loading</div>;
return <>{user.theId}</>
This way it should work
but keep in mind that you must render your app only if theId in the state is present, which will mean your user is properly fetched.
const [state, setState] = useState({ theId: '', isAutorized: false })
const getUser = async () => {
try {
const idResp = await fetch("http://localhost:4200/get-user");
const theId = await idResp.json();
const authResp = await fetch("http://localhost:4200/get-valid-users");
const allUsers = await authResp.response.json();
const validIds = allUsers.map(({ id }) => id);
const isAuthorised = validIds.includes(theId);
setState({ theId, isAuthorised })
} catch (err) {
console.error(err.message);
}
}
useEffect(() => {
getUser();
}, []);
if (!state.theId) return <div>Loading</div>;
if (state.theId && !isAuthorized) return <AccessNotAllowed />
return <Home />

Promise inside a loop inside an async function

I am working on a project using react and firebase and redux and I have some items that did created by a user. I'm storing the id of the user in the item object so i can populate the user later when i get the item to display.
Now I'm trying to get the items and modify them by replacing the user id with the actual info about the user but I have a promises problem. In my code I just get an empty array which mean the modification didn't get resolved before I return the final result.
export const getItems = () => {
return (dispatch, getState, { getFirebase }) => {
const firestore = getFirebase().firestore();
const items = [];
const dbRef = firestore.collection('items').orderBy('createdAt', 'desc').limit(2);
return dbRef
.get()
.then((res) => {
const firstVisible = res.docs[0];
const lastVisible = res.docs[res.docs.length - 1];
async function getData(res) {
/////////////////////////////////////////////// how to finish this code befor jumping to the return line
await res.forEach((doc) => {
firestore
.collection('users')
.doc(doc.data().owner)
.get()
.then((res) => {
items.push({ ...doc.data(), owner: res.data() });
});
});
////////////////////////////////////////////////
return { docs: items, lastVisible, firstVisible };
}
return getData(res);
})
.catch((err) => {
console.log(err);
});
};
};
I don't get exactly what you are trying to do, but I would suggest putting some order to make your code easy to read and work with.
You can use for of to manage async looping. I suggest something like this, disclaimer, I did it at the eye, problably there are some errors, but you can get the idea.
const getAllDocs = function (data) {
let temp = [];
data.forEach(function (doc) {
temp.push(doc.data());
});
return { data: temp };
};
const getDoc = snap => (snap.exists ? { data: snap.data() } : {});
export const getItems = () => {
return async (dispatch, getState, { getFirebase }) => {
const firestore = getFirebase().firestore();
const dbRef = firestore.collection('items').orderBy('createdAt', 'desc').limit(2);
const usersRef = firestore.collection('users');
let temps = [];
const { data: items } = await dbRef.get().then(getAllDocs);
const firstVisible = items[0];
const lastVisible = items[items.length - 1];
for (const item of items) {
const { data: user } = await usersRef.doc(item.owner).get().then(getDoc);
const owner = {
/* whatever this means*/
};
temps.push({ ...user, owner });
}
return { docs: temps, lastVisible, firstVisible };
};
};
The problem is that an array of Promises is not itself a Promise -- so awaiting it will be a no-op.
You can solve this using Promise.all if you want to load them all asynchronously.
const items = await Promise.all(res.map(async (doc) => {
const res = await firestore.collection('users').doc(doc.data().owner).get();
return { ...doc.data(), owner: res.data() };
});
Otherwise you can await in a for loop as suggested in other answers.

How to return an array received from fetching api data in a .then statement?

I'm trying to export an array inside a .then statement but its not working. I have no clue how to make it work otherwise. Actually I'm just trying to set my initial state in redux to this static data I am receiving from the movie database api.
import { API_URL, API_KEY } from '../Config/config';
const urls = [
`${API_URL}movie/popular?api_key=${API_KEY}&language=en-US&page=1`,
`${API_URL}movie/popular?api_key=${API_KEY}&language=en-US&page=2`,
]
Promise.all(urls.map(items => {
return fetch(items).then(response => response.json())
}))
.then(arrayOfObjects => {
var arr1 = arrayOfObjects[0].results;
var arr2 = arrayOfObjects[1].results;
export var movieData = arr1.concat(arr2);
}
)
You can try with a function. like this:
import { API_URL, API_KEY } from '../Config/config';
export const getMovies = () => {
const urls = [
`${API_URL}movie/popular?api_key=${API_KEY}&language=en-US&page=1`,
`${API_URL}movie/popular?api_key=${API_KEY}&language=en-US&page=2`,
]
const promises = urls.map(url => {
return new Promise((reject, resolve) => {
fetch(url).then(res => res.json())
.then(res => resolve(res.results))
})
})
return Promise.all(promises)
}
// other file
import {getMovies} from 'YOUR_API_FILE.js';
getMovies().then(moviesArr => {
// your business logics here
})
It's not clear where this code is in relation to your state/reducer, but ideally you should be using action creators to deal with any API calls and dispatch state updates, and those action creators can be called from the component.
So, initialise your state with an empty array:
const initialState = {
movies: []
};
Set up your reducer to update the state with MOVIES_UPDATE:
function reducer(state = initialState, action) {
const { type, payload } = action;
switch (type) {
case 'MOVIES_UPDATE': {
return { ...state, movies: payload };
}
}
}
You can still use your function for fetching data:
function fetchData() {
return Promise.all(urls.map(items => {
return fetch(items).then(response => response.json());
}));
}
..but it's called with an action creator (it returns a function with dispatch param), and this action creator 1) gets the data, 2) merges the data, 3) and dispatches the data to the store.
export function getMovies() {
return (dispatch) => {
fetchData().then(data => {
const movieData = data.flatMap(({ results }) => results);
dispatch({ type: 'MOVIES_UPDATE', payload: movieData });
});
}
}
And it's called from within your component like so:
componentDidMount () {
this.props.dispatch(getMovies());
}
You can modify the code as below:
import { API_URL, API_KEY } from '../Config/config';
let movieData='';
exports.movieData = await (async function(){
const urls = [
`${API_URL}movie/popular?api_key=${API_KEY}&language=en-US&page=1`,
`${API_URL}movie/popular?api_key=${API_KEY}&language=en-US&page=2`,
];
const arrayOfObjects = await Promise.all(urls.map(items => {
return fetch(items).then(response => response.json())
}));
return arrayOfObjects[0].results.concat(arrayOfObjects[1].results);
})();

Categories

Resources