How to query multiple values for a given key in firebase - javascript

I attempted to use an 'OR' || statement in the equalTo() method, but this does not seem to work. Do I have to make a separate call for each value or is there a way to make conditional queries in firebase?
export const startSetContent = () => {
return (dispatch, getState) => {
const ref = database.ref("content");
return ref
.orderByChild("category")
.equalTo('one-act-play' || 'ten-min-play' || 'full-length-play')
.once('value')
.then(snapshot => {
const content = [];
snapshot.forEach(childSnapshot => {
content.push({
id: childSnapshot.key,
...childSnapshot.val()
});
});
dispatch(setContent(content));
});
};
};

What you typed there was a logical OR that JavaScript the programming language interprets. The result of the expression:
'one-act-play' || 'ten-min-play' || 'full-length-play'
is going to be simply:
'one-act-play'
So, Firebase Realtime Database will not see what you're trying to tell it.
Firebase Realtime Database has no concept of a kind of query that has ORs in it. You will have to query each type of thing separately and merge them together in your code as needed.

There is no concept of doing that you are asking for. You can do a turn around in which you can get the values on basis of one filter one-act-play using equalTo and rest you can filter on user end like:
export const startSetContent = () => {
return (dispatch, getState) => {
const ref = database.ref("content");
return ref
.orderByChild("category")
.equalTo('one-act-play')
.once('value')
.then(snapshot => {
const content = [];
Object.keys(snapshot.val()).map(k => {
if(k == 'ten-min-play' || k == 'full-length-play'){
snapshot.forEach(childSnapshot => {
content.push({
id: childSnapshot.key,
...childSnapshot.val()
});
});
}
dispatch(setContent(content));
});
};
};
Or you can use library made to fulfil this kind of requirement Querybase

Related

Firebase Realtime database query from v8 to v9 SDK

I have a Firebase Realtime database query written using Firebase version 8 that I'm looking to migrate over to the v9 SDK.
export const getSingleDataWithQuery = async ({ key, email, criteria }) => {
if (!criteria) return;
const snapshot = await realTimeDb
.ref()
.child(key)
.orderByChild(query)
.equalTo(criteria)
.get();
const val = snapshot.val();
if (val) {
const keys = Object.keys(val);
return val[keys[0]];
}
return null;
};
In this example:
key would be the 'users' collection
the email field is looking for users by their email
and the criteria is the user's actual email (jane.doe#gmail.com)
Using Firebase's Read data once and Sorting data documentation I managed to narrow it down to perhaps being this, but I'm not sure if it's correct:
export const getSingleDataWithQuery = async ({ key, query, criteria }) => {
if (!criteria) return;
const dbRef = query(ref(realTimeDb, key), orderByChild(email), equalTo(criteria));
get(dbRef).then(snapshot => {
if (snapshot.exists()) {
const val = snapshot.val();
if (val) {
const keys = Object.keys(val);
return val[keys[0]];
}
}
});
return null;
};
Aside from the fact that you may have swapped query and email in the fragments, the only difference is in the way you handle the asynchronous database call and that likely explains the difference. Since you use then in the second snippet, the function doesn't actually return a promise and so calling it with await it in the caller won't have any effect.
To make those the same again, use await in the second snippet too, instead of then:
export const getSingleDataWithQuery = async ({ key, query, criteria }) => {
if (!criteria) return;
const dbRef = query(ref(realTimeDb, key), orderByChild(email), equalTo(criteria));
const snapshot = await get(dbRef); // 👈
if (snapshot.exists()) {
const val = snapshot.val();
if (val) {
const keys = Object.keys(val);
return val[keys[0]];
}
}
return null;
};

How to get all documents in Firestore (v9) sub collection

I'm trying to get all documents in all sub-collection with Firebase V9, so I used collectionGroup to retrieve the data.
Here is my Firestore architecture :
bots (collection)
| id (doc ID)
| order_history (sub collection)
| id (doc ID)
createdBy: uid (user uid)
And this is how I try to get documents :
const [orders, setOrders] = useState([]);
const { user } = useAuth();
const getOrders = async () => {
const orders = await getDocs(query(collectionGroup(db, 'order_history'), where('createdBy', '==', user.uid)));
setOrders(orders.docs.map((doc) => ({
...doc.data()
})))
}
useEffect(() => {
getOrders();
console.log('orders ', orders); // return []
}, []);
This code returns an empty array.
Did I do something wrong?
I think your getOrders function is asynchronus function.
If you want debug log I think you should waiting for getOrders completed then orders had been updated.
Ex:
useEffect(() => {
console.log('orders ', orders);
}, [orders]);
Your getOrders anonymous method execution requires an explicit return statement if there's more than 1 statement. Implicit returns work when only a single statement exists (and after some testing, return await X doesn't appear to work either).
const getOrders = async () => {
const orders = await getDocs(...);
setOrders(orders.docs.map(...))
}
Needs to be
const getOrders = async () => {
const orders = await getDocs(...);
return setOrders(orders.docs.map(...))
}

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:

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

ReactJS how to wait for all API calls to be ended in componentDidMount of simple component

I'm using latest react and very basic app which calls 3rd party service API which actually is not well designed in meaning of following.
I have to execute one call which return list and then have to iterate and call other end point to get data for item from list and then again in data have new list for which I have to call 3rd API end point.
After I receive all data I combined it to one items array and place it in state in componentDidMount function but this final step works only if I surround it with setTimeout.
Is there some elegant way to do that?
I'm using fetch and really pure react components, have my own simple service from where I call API, here is some code parts...
items[tag].sensors = [];
API.getObjects(sessionData, userDetails, tag).then(links => {
Object.keys(links.link).forEach(link => {
API.getObjects(sessionData, userDetails, link).then(objLink => {
Object.keys(objLink.link).forEach(function (key) {
let obj = objLink.link[key];
if (obj && obj.type === 'sensor') {
API.getSensorNames(sessionData, key).then(response => {
const sensor = response.sensor;
// some sensor calculations....
items[tag].sensors.push(sensor);
});
}
});
});
});
});
// this part only works if it's surrounded with timeout
setTimeout(function() {
let processedItems = [];
for (var key in items) {
if (items.hasOwnProperty(key)) {
processedItems.push(items[key]);
}
}
self.setState({
items: processedItems
});
}, 1000);
Thanks in advance.
Simply, You can use Promise to wait until you get values from the API call, therefore you will put your code in function like this
function prepareItems() {
items[tag].sensors = [];
return new Promise((resolve, reject) => {
API.getObjects(sessionData, userDetails, tag).then(links => {
Object.keys(links.link).forEach(link => {
API.getObjects(sessionData, userDetails, link).then(objLink => {
Object.keys(objLink.link).forEach(function(key) {
let obj = objLink.link[key];
if (obj && obj.type === "sensor") {
API.getSensorNames(sessionData, key).then(response => {
const sensor = response.sensor;
// some sensor calculations....
items[tag].sensors.push(sensor);
// whenever you set resolve it will end the promise
//and pass the result it to the then function
resolve(items)
});
}
});
});
});
});
});
}
and use then to get the result from the prepareItems function after its resolved
prepareItems().then(items => {
//Do what ever you want with prepared item
})
What about using async/await operators.
These operators allows you to wait until the response is ready.
You can use this kind of helper function.
getItems = async (...) => {
...
items[tag].sensors = []
const links = await API.getObjects(sessionData, userDetails, tag)
Object.keys(links.link).forEach(async (link) => {
const objLink = await API.getObjects(sessionData, userDetails, link)
Object.keys(objLink.link).forEach(async (key) => {
let obj = objLink.link[key]
if (obj && obj.type === 'sensor') {
const response = await API.getSensorNames(sessionData, key)
const sensor = response.sensor
items[tag].sensors.push(sensor)
}
})
})
this.setState({ items })
}
Also you can see this great documentation.

Categories