Firebase Real Time Database read data - javascript

I'm building something like a Messenger using React + Redux and Real-Time Database from Firebase.
I'm used to retrieve data from an API like this:
export const fetchContacts = () => {
return async dispatch => {
dispatch(fetchContactsStart());
try {
const response = await axios.get('....');
dispatch(fetchContactsSuccess(...));
} catch(e) {
dispatch(fetchContactsFail(...));
}
}
}
Now in firebase I have the following data:
enter image description here
What I need is to get all user contacts and then get user info associated with those contacts and build a structure like:
[{email: ..., username: ...}, {email: ..., username: ...}, ...]. After getting all the data in that format I want to dispatch the action to my reducer.
Right now I have the following code:
export const fetchContacts = () => {
return dispatch => {
dispatch(fetchContactsStart());
const userEmail = firebase.auth().currentUser.email;
let data = [];
firebase.database().ref(`users_contacts/${Base64.encode(userEmail)}`)
.on('value', contacts => contacts.forEach(contact => {
firebase.database().ref(`users/${Base64.encode(contact.val().contact)}`)
.on('value', user => {
data.push({ email: contact.val().contact, username: user.val().username });
console.log(data)
})
}
))
}
}
This works but I don't know how to dispatch the action only when the data is fully formed, is there any solution for this? Thanks for the help!

When you're waiting for multiple asynchronous operations to complete, the solution is usually to use Promise.all. In this case, that'd look something like this:
export const fetchContacts = () => {
return dispatch => {
dispatch(fetchContactsStart());
const userEmail = firebase.auth().currentUser.email;
let data = [];
firebase.database().ref(`users_contacts/${Base64.encode(userEmail)}`).once('value', contacts => {
let promises = [];
let map = {};
contacts.forEach(contact => {
const contactId = Base64.encode(contact.val().contact);
map[contactId] = contact.val().contact;
promises.push(firebase.database().ref(`users/${contactId}`).once('value'));
});
Promise.all(promises).then((users) => {
users.forEach((user) =>
data.push({ email: map[user.key], username: user.val().username });
})
// TODO: dispatch the result here
}
))
}
}
The main changes in here:
Now uses once() for loading the user data, so that is only loads once and returns a promise.
Uses Promise.all to wait until all profiles are loaded.
Added a map to look up the email address in the inner callback.

Related

Firestore iterate over an object within a document's data REACT.JS

I'm trying to add some data inside the bookChapters path but it doesn't work, some suggestions?
export const createNewChapter = (bookId, inputText) => {
return async dispatch => {
dispatch(createNewChapterStart());
try {
const chaptersList = [];
firebase
.firestore()
.doc(`Users/${bookId}/bookChapters/${inputText}`)
.onSnapshot(querySnapshot => {
querySnapshot.forEach(doc => {
const chapters = doc.data().bookChapters;
Object.keys(chapters).forEach(k => {
chaptersList.push({ key: k, name: inputText });
});
});
});
dispatch(createNewChapterSuccess(inputText));
} catch (error) {
dispatch(createNewChapterFail(error));
console.log(error);
}
};
};
When you use collection(), it returns a CollectionReference. In this case, it's pointing towards a sub-collection 'bookChapters' but it's a map as in your screenshot. If you want to iterate over that map, you need to fetch that document first and then read the bookChapters field.
const chaptersList = [];
firebase
.firestore()
.doc(`Users/${bookId}`)
.onSnapshot(querySnapshot => {
querySnapshot.forEach(doc => {
const chapters = doc.data().bookChapters
Object.keys(chapters).forEach((k) => {
chaptersList.push({key: k, name: chapters[k]});
})
});
});
It might be better to store the list as an Array if you want to store all chapters in the same doc.
If you were trying to create a sub-collection, you can create one by clicking this button:

Keep getting values from function with realtime listener

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

How to store API response data for analysis in javascript

I am attempting to build a bot that will periodically poll an API using axios for the price of multiple cryptocurrencies across multiple exchanges. I need to be able to then look at this stored data to analyse price differences of a given token between multiple exchanges to calculate if there is profit to be made if I were to purchase it on one and then sell on another.
So far I am able to get access to the price data and console.log it as the request is returned, however I need to add this data to an array so that it can be analysed at a later point. I understand that I will need to use promises to handle this but I find it slightly confusing.
The following code immediately outputs an empty array and then outputs all the individual prices. How can I gain access to those values at a later point in the code?
const axios = require('axios');
const { exchanges } = require('./resources/exchanges.json');
const { currencies } = require('./resources/currencies.json');
const buyCurrency = currencies.buy[0];
const poll = async () => {
const data = new Array();
await currencies.sell.forEach(async sellCurrency => {
await exchanges.forEach(async exchange => {
try {
const allOtherExchanges = exchanges.filter(x => x !== exchange).map(x => `,${x}`).join();
const response = await axios.get(`https://api.0x.org/swap/v1/quote?buyToken=${buyCurrency}&sellToken=${sellCurrency}&sellAmount=1000000000000000000&excludedSources=0x${allOtherExchanges}`)
if (response && response.data.price) {
console.log(exchange, sellCurrency, response.data.price)
data.push({
exchange,
price: response.data.price
});
}
} catch {}
});
});
console.log(data);
};
poll()
One of the solutions would be as follows:
const axios = require('axios');
const { exchanges } = require('./resources/exchanges.json');
const { currencies } = require('./resources/currencies.json');
const buyCurrency = currencies.buy[0];
const poll = async () => {
const data = new Array();
const promises = [];
currencies.sell.forEach(sellCurrency => {
exchanges.forEach(exchange => {
const allOtherExchanges = exchanges.filter(x => x !== exchange).map(x => `,${x}`).join();
promises.push(
axios.get(`https://api.0x.org/swap/v1/quote?buyToken=${buyCurrency}&sellToken=${sellCurrency}&sellAmount=1000000000000000000&excludedSources=0x${allOtherExchanges}`)
.then(response => {
if (response && response.data &&response.data.price) {
console.log(exchange, sellCurrency, response.data.price)
data.push({
exchange,
price: response.data.price
});
}
}).catch(err => console.error(err))
);
});
});
await Promise.all(promises);
console.log(data);
};
poll();

setState in nested async function - React Hooks

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

How do I use a document's field from one collection to retrieve another document field from a different collection?

Here is how my database is structured:
challenges table &
users table
Here is the error I'm getting: error image
I want to use the "created_by" field which is also a document id for the users table, where I want to retrieve both the display name and the photo URL.
I'm not all the way sure how promises work and I have a feeling that is why I'm struggling, but the code I have thus far is below:
Data Retrieval:
UsersDao.getUserData(ChallengesDao.getChallenges().then(result => {return result['author'][0]})).then(result => {console.log(result)})
Challenges DAO:
export default class ChallengesDao {
static async getChallenges() {
const db = require('firebase').firestore();
// const challenges = db.collection('challenges').limit(number_of_challenges)
// challenges.get().then(())
const snapshot = await db.collection('challenges').get()
const names = snapshot.docs.map(doc => doc.data().name)
const createdBy = snapshot.docs.map(doc => doc.data().created_by)
const highScores = snapshot.docs.map(doc => doc.data().high_score.score)
return {challengeName: names, author: createdBy, score: highScores}
}
Users DAO:
const db = require('firebase').firestore();
export default class UsersDao {
static async getUserData(uid: string) {
let userData = {};
try {
const doc = await db
.collection('users')
.doc(uid)
.get();
if (doc.exists) {
userData = doc.data();
} else {
console.log('User document not found!');
}
} catch (err) {}
return userData;
}
}
You're getting close. All that's left to do is to call getUserData for each UID you get back from getChallenges.
Combining these two would look something like this:
let challenges = await getChallenges();
let users = await Promise.all(challenges.author.map((uid) => getUserData(uid));
console.log(challenges.challengeName, users);
The new thing here is Promise.all(), which combines a number of asynchronous calls and returns a promise that completes when all of them complete.
Your code looks a bit odd to me at first, because of the way you return the data from getChallenges. Instead of returning three arrays with simple values, I'd recommend returning a single array where each object has three values:
static async getChallenges() {
const db = require('firebase').firestore();
const snapshot = await db.collection('challenges').get();
const challenges = snapshot.docs.map(doc => { name: doc.data().name, author: doc.data().created_by, score: doc.data().high_score.score });
return challenges;
}
If you then want to add the user name to each object in this array, in addition to the UID that's already there, you could do:
let challenges = await getChallenges();
await Promise.all(challenges.forEach(async(challenge) => {
challenge.user = await getUserData(challenge.author);
});
console.log(challenges);

Categories