Get array of objects from real time data snapshot - Cloud Firestore - javascript

I'm trying to fetch real time data from Cloud Firestore using the below code.
export const getRealTimeData = () =>
db
.collection('posts')
.onSnapshot(
(querySnapshot) => {
const posts: any = [];
querySnapshot.forEach((doc) =>
posts.push(Object.assign({
id: doc.id
}, doc.data()))
);
},
);
};
And, I want to use the resultant array to display the data on UI. When I'm doing this, the resultant array is a function but not the actual array of data.
const posts = getRealTimeData();
Here's what I get when I log posts
function () {
i.kT(), o.al(s);
}
Could anyone please point where I went wrong?

Realtime listeners added with onSnapshot() are not compatible with returning values from function calls. That's because they continue to generate new results over time, and would never really "return" anything once. You should abandon the idea of making a synhronous getter type function in this case - they just don't work for what you're trying to do.
Ideally, you would use an architecture like Redux to manage the updates as they become available. Your realtime listener would dispatch query updates to a store, and your component would subscribe to that store that to receive those updates.
If you don't want to use Redux (which is too bad - you really should for this sort of thing), then you should wrap your query inside a useEffect hook, then have your listener set a state hook variable so your component can receive the updates.

Related

React Custom Hook function keeps recalling

According to the thread below,
useCustomHook being called on every render - is something wrong with this
It says it is completely normal to keep calling the custom hook function every time React re-renders.
My questions are, if it affects on a performance side when returning an array from this Custom Hook function( Not when fetching API and receiving data ) which contains a lot of values.
If so, how to prevent it ( How to let this Custom Hook function run only once )?
Here is my Custom Hook code, it returns an array which contains around 5000 string values.
function FetchWords(url: string) {
const [data, setData] = useState<string[]>([]);
useEffect(() => {
fetch(url)
.then((words) => words.text())
.then((textedWords) => {
setData(textedWords.replace(/\r\n/g, "\n").split("\n"));
});
}, []);
const expensiveData = useMemo(() => data, [data]);
return expensiveData;
}
export default FetchWords;
My Main js
const wordLists: any[] = useFetch(
"https://raw.githubusercontent.com/charlesreid1/five-letter-words/master/sgb-words.txt"
);
CustomHooks should start with word use...
You don't need useMemo in your hook, simply return data state.
Your hook makes the fetch call only once, so no problem there as the effect has empty dependency, so it runs once after first render.
The hook stores the array of 5000 entries once in data state and returns the same reference each time your custom hook is called during component re-renders. There is no copy operation, so you don't need to worry about that.
If you only want to fetch 100 entries for example, then your backend needs to provide that api.
Hope this resolves your queries as it is not very clear what is your doubt.
If you are worried about bringing all this data at the same time, you can indicate from the backend that they send you a certain number of records and from the frontend you can manage them with the pagination.
the use of useMemo is superfluous.
the useEffect that you are using will only be rendered ONCE, that is, it will only call the 5,000 registers that you mention only once

I want to access my state variable from one component to other

I have a react query which writes the state variable- follower, and I want to access this variable in other component to find its .length can someone tell me how do I do it
const ModalFollower = ({profile}) => {
const [follower,setFollower] = useState([])
const {
data: followerName,
isLoading: followerLoading,
isFetching: followerFetching
} = useQuery(["invitations", profile?.id], () => {
getFollowers(profile?.id).then((response) => {
if (response) {
setFollower(response);
}
});
});
return(
{
!followerLoading && (
follower.map((e) => {
return(<>
<p>{e.requested_profile.Userlink}</p>
</>}
)
}
)
I want to access the length of follower in some other component
There is no need to copy data from react-query to local state, because react-query is a full-blown state manager for server state. As long as you use the same query key, you will get data from its cache. This is best abstracted away in custom hooks.
Please be aware that with the default values, you will get a "background refetch" if a new component mount, so you will see two network requests if you use it twice. That might look confusing at first, but it is intended, as it is not react-query's primary goal to reduce network requests, but to keep your data on the screen as up-to-date as possible. So when a new component mounts that uses a query, you'll get the stale data from the cache immediately, and then a background refetch will be done. This procedure is called stale-while-revalidate.
The best way to customize this behaviour is to set the staleTime property to tell react-query how long your resource is "valid". For that time, you will only get data from the cache if available. I've written about this topic in my blog here: React Query as a State Manager.
React Query also provides selectors, so if your second component is only interested in the length, this is what my code would look like:
const useInvitations = (profile, select) =>
useQuery(
["invitations", profile?.id],
() => getFollowers(profile?.id),
{
enabled: !!profile?.id
select
}
)
Note that I also added the enabled property because apparently, profile can be undefined and you likely wouldn't want to start fetching without that id.
Now we can call this in our main component:
const ModalFollower = ({profile}) => {
const { data } = useInvitations(profile)
}
and data will contain the result once the promise resolves.
In another component where we only want the length, we can do:
const { data } = useInvitations(profile, invitations => invitations.length)
and data will be of type number and you will only be subscribed to length changes. This works similar to redux selectors.

How can I stop extra rerenders in my component when I rely on a usePrevious?

So my googling did not return any useful results, so sorry if I ask a duplicate question.
So I am listening to my redux-store in react with 2 selectors. 1 to get the oldest timestamp we have, and another to get all messages in the store.
// returns an array
const messages = useSelector(
(store) => selectMessages(store, chatId),
shallowEqual
);
const [showGetMore, setShowGetMore] = useState(true);
// returns a firebase timestamp
const oldestMessage= useSelector((store) =>
selectOldestMessage(store, chatId)
);
// uses the react hook usePrevious code
const previousOldestTimestamp = usePrevious(
// if oldestMessage is undefined still, set it as null,
// so the getMore doesn't disappear
oldestMessage || null
);
useEffect(() => {
if (previousOldestTimestamp === oldestMessage) {
// if timestamps are equal, there are no more old messages to get
setShowGetMore(false);
}
}, [
previousOldestTimestamp,
oldestMessage,
setShowGetMore,
]);
So when I click to get more messages, i make a call that calls firebase for more messages, then it sends 2 actions, 1 to update the messages object, and another to update the oldest message field.
However, using a props/state watcher, my selectOldestMessages sometimes rerenders with the same number of objects in the array, even though I am using shallowEquals to prevent that, and the useEffect is always running and declaring that the timestamps are the same, when in the store they are always updating correctly.
So I assume it's because the 2 actions I dispatch are triggering the component to re render 2 times which causes the usePrevious hook to run 2 times which breaks my code because it should only being re rendered 1 time.
But if I am dead wrong, I would love to know.
Does anyone have a better solution for this, or see a bad react coding pattern?

How to enable persistence on reactfire?

I'd like to implement Firestore offline persistence on my PWA React app using the reactfire library.
const firestore = useFirestore().enablePersistence();
let documentReference = firestore
.collection("food")
.doc("milkshake");
const { data } = useFirestoreDocData(documentReference);
but running the code i get an error:
FirebaseError: Firestore has already been started and persistence can no longer be enabled. You can only enable persistence before calling any other methods on a Firestore object.
This component is wrapped inside a <Suspense> as mentioned in the documentation
That database read is the only one that i make in the entire app, how can i solve?
Edit.
Using the example that #Ajordat gave, I've imported the preloadFirestore function inside the App component I do get an error:
"Cannot read property 'name' of undefined".
Whereas adapting (because I cannot use hooks inside the fetch function)
the example from #DougStevenson: I've imported useFirestore function in the App component (in order to get the Firestore object) to enable persistence, and then importing it (useFirestore) into my component in order to retrieve the data, but now, I get the same error as before,
Firestore has already been started and persistence can no longer be enabled.
Edit 2:
I've tried to enablePersistence without errors, thank guys, this is my approach, let me know if it is the best:
const firestore = useFirestore();
React.useEffect(() => {
firestore.enablePersistence();
}, []);
And in my custom component:
let docRef = useFirestore()
.collection("food")
.doc("milkshake");
let document = useFirestoreDocDataOnce(docRef);
console.log(document)
But now I do have a problem, when I log the document, the data are not emitted instantly, yeah I know that it is an asynchronous operation, but the component is wrapped inside a <Suspense>, in this way:
<Suspense fallback={<div>Loading</div>}>
<FoodComponent foodName={"Milkshake"} />
</Suspense>
But I don't see the loading text before the component is actually rendered.
Does the suspense fragment show the fallback component only while is loading the function (useFirestore) and not the actual data?
Well, I've solved, have to destructure the data, doing like that:
let docRef = useFirestore()
.collection("food")
.doc("milkshake");
let { data: document } = useFirestoreDocData(docRef);
console.log(document)
On other JavaScript libraries for Firestore, enablePersistence() returns a promise. That means it will complete some time in the future, with no guarantees how long it will take. If you're executing the query immediately after you call enablePersistence(), without waiting for the returned promise to become fulfilled, then you will see this error message. That's because the query "beats" the persistence layer and effectively executes first.
You will have to figure out how to use that promise to wait until it's OK to make that query with persistence enabled. For example:
seFirestore().enablePersistence()
.then(() => {
let documentReference = firestore
.collection("food")
.doc("milkshake");
const { data } = useFirestoreDocData(documentReference);
})
.catch(error => {
console.error("enablePersistence failed", error);
})
Notice how the query will complete only after the persistence is fully enabled.
Thanks for the suggestion guys #DougStevenson and #Ajordat
In app component:
import { useFirestore } from "reactfire"
...
const firestore = useFirestore();
React.useEffect(() => {
firestore.enablePersistence();
}, []);
In your custom component, where you want to use Firestore:
import { useFirestore, useFirestoreDocData /* or what you want to use */ } from "reactfire"
let docRef = useFirestore()
.collection("food")
.doc("milkshake");
let { data: document } = useFirestoreDocData(docRef);
console.log(document);

How to execute a sort after loading in data into an array in UseEffect - React Native

I'm trying to create a chat app and there is a small issue. Whenever I load in my messages from firebase, they appear in the chat app in unsorted order, so I'm attempting to sort the messages by timestamp so they appear in order. I can do this if I move the sort and setMessages within onReceive of useEffect, but I feel like this will be pretty inefficient because it sorts and setsMessages a separate time for each message that's retrieved from firebase. I want to just do it all at the end after all the messages are loaded into the array.
Right now with my logs, I get this:
[REDACTED TIME] LOG []
[REDACTED TIME] LOG pushing into loadedMessages
[REDACTED TIME] LOG pushing into loadedMessages
So it's printing the (empty) array first, then loading in messages. How can I make sure this is done in the correct order?
useEffect(() => {
// Gets User ID
fetchUserId(getUserId());
const messagesRef = firebase.database().ref(`${companySymbol}Messages`);
messagesRef.off();
messagesRef.off();
const onReceive = async (data) => {
const message = data.val();
const iMessage = {
_id: message._id,
text: message.text,
createdAt: new Date(message.createdAt),
user: {
_id: message.user._id,
name: message.user.name,
},
};
loadedMessages.push(iMessage);
console.log('pushing into loadedMessages');
};
messagesRef.on('child_added', onReceive);
loadedMessages.sort(
(message1, message2) => message2.createdAt - message1.createdAt,
);
console.log(loadedMessages);
return () => {
console.log('useEffect Return:');
messagesRef.off();
};
}, []);
I think that the perspective is a bit off.
The right way to do so will be to fetch the firebase data sorted.
Firebase has a built-in sort, although it does come with its limitations.
In my opinion, you sould try something like:
const messagesRef = firebase.database().ref(`${companySymbol}Messages`);
messagesRef.orderByChild("createdAt").on("child_added", function(snapshot) {
// the callback function once a new message has been created.
console.log(snapshot.val());
});
And if I may add one more thing, to bring every single message from the down of time can be a bit harry once you've got over a thousand or so, so I would recommend limiting it. that can be achieved using the built-in limit function limitToLast(1000) for example.
Good luck!
Well, the name of the database is "Realtime Database". You are using the "child_added" listener which is going to be triggered every time a new object gets added to the Messages collection. The onReceive callback should do the sorting - otherwise the messages won't be in the correct order. Yes, that is inefficient for the first load as your "child_added" will most probably be triggered for every item returned from the collection and you'll be repeating sorting.
What you could explore as alternative is to have a .once listener: https://firebase.google.com/docs/database/web/read-and-write#read_data_once the first time you populate the data in your app. This will return all the data you need. After that is complete you can create your "child_added" listener and only listen for new objects. This way onReceive shouldn't be called that often the first time and afterwards it already makes sense to sort on every new item that comes in.
Also have a look at sorting: https://firebase.google.com/docs/database/web/lists-of-data#sorting_and_filtering_data
You might be able to return the messages in the correct order.
And also - if you need queries - look at firestore...

Categories

Resources