I am working with ReactJS and Google Firestore. I have a component called GameEntryForm, where you can select from a list of users stored in Firestore. In order to get this list, when I render the GameEntryForm component, I make a query to Firestore. Below is how I am getting the list.
I was wondering if there was a better or faster way to do this. My concern is that as the number of users increases, this could be a slow operation.
function GameEntryForm() {
// prevent rendering twice
const effectRan = useRef(false);
const [usersList, setUsersList] = useState(new Map());
useEffect(() => {
if (effectRan.current === false) {
const getUsers = async () => {
const q = query(collection(firestore, "users"));
const querySnapshot = await getDocs(q);
querySnapshot.forEach((doc) => {
setUsersList(new Map(usersList.set(doc.data().uid, doc.data())));
});
};
getUsers();
return () => {
effectRan.current = true;
};
}
}, []);
}
Your code looks fine at first glance, but
here are many ways to mitigate this issue some of them are as follows:
Implement Pagination Functionality to limit the number of documents that are returned by the query, for more about this topic go through this docs
Use Firestore Offline Caching feature through persistence like one provided here. I understand that your user will be added constantly so there’s not much improvement with this method but you can trigger a new request to the db based on the changed type. This is nicely explained in this thread
You can also use the above caching with a global state management solution(Redux, Context API) and only fetch the list of users once. This way, the list of users would be accessible to all components that need it, and you would only have to make the query once. Someone has created an example for how this will work although not using firestore though.
Last but not least use Real Time lister to View changes between snapshots as provide here in official docs This works great with the offline Caching option.
Related
I always run into situations where I need to force rerender, while I'm still in the execution of some function, so I developed my solution to this and I need to know if this is right or there is a simpler way to achieve the same goal.
I rely on the state variable my_force_update, then I change it to a random value when I want to enforce a change. like:
const [my_force_update, setMyForceUpdate] = useState(0);
useEffect(()=>{}, [my_force_update]);
const handleSubmit = async () =>{
await prm1();
stMyForceUpdate(Math.random()); // enforcing the effect
await prom2();
....
}
so I have been able to enforce re-render (by enforcing the effect) while I'm still in the handleSubmit execution.
is there a simpler way? or, did I mistakenly understand the concepts of React?
update
The issue is that I have a checkout form, and I need it to be a signup form at the same time, and there is also a login component on the page.
so I need to populate the form fields with the account if information in case of login and in case of sign up.
The steps are as follow:
if user login => populate form (per fill it with user info) => move to payment.
if user fill out the form manually:
create an account.
authenticate the new user.
update the user account.
repopulate form (with data from user account).
move to payment.
so I have this function that needs to listen to the login and signup:
const token = useSelector(_token);
const loggedIn = useSelector(_loggedIn);
const profile = useSelector(_profile);
useEffect(() => {
/**
* Pre-fill the form inputs
*/
(async () => {
const r = await dispatch(fetchUserInfo());
setFormProfile(profile); // address is not updated yet
setFormAddress(r?.result?.address);
})();
}, [loggedIn, forceUpdate]);
now, there are no issues with the login process, the only problem is with the signup:
at step 2, when authenticating the user, its account is empty.
so the loggedIn changes to true when the profile is empty so I got empty form.
after updating the profile, loggedIn will not change, so I need another variable to trigger the effect again.
I tried to listen to profile here, but I got an infinite loop.
and here is the checkout flow related to the signup:
...
if (!loggedIn) {
const signupResponse = await dispatch(signupUser(params));
loginResponse = await dispatch(login(formProfile?.email, password));
}
const updateProfileResponse = await saveChangesToProfile();
// update user profile with the information in the checkout form.
...
then save changes to the profile:
const saveChangesToProfile = async () => {
const r = await dispatch(fetchUserInfo());
const addressID = r?.result?.address_id;
const res1 = await dispatch(updateUserAddress(addressID, { ID: addressID, ...formAddress }));
const res = await dispatch(UpdateUser(r?.result?.ID, formProfile));
setForceUpdate(Math.random()); // force re-render to re-populate the form.
setSuccess("Information saved to your profile!");
return res;
};
Update 2
The question is general, I solved the issue in another way days ago (involving changes to the server routes). and I'm asking the question in a general way to get some knowledge, not for others to do the work for me.
In general, you should avoid having to force an update in React but instead use existing React features to accomplish your goal. That being said, there are simple ways to force a re-render in react. You mentioned in the second update that you are looking for more general solutions - so I will provide them here.
However, please bear in mind that this topic has been discussed extensively in other stack overflow questions (I will provide links).
Forcing Re-Render using component.forceUpdate(callback)
The react docs actually list a simple way to force a component to reload (provided you maintain a reference to it). You can find more information here, but essentially it forces your component to re-render and then makes a call to the callback argument.
Forcing Re-Render using hooks
There are multiple stack overflow questions that provide simple code snipets that can force a react component to re-render by using hooks. This answer for example by #Qwerty demonstrates 2 simple code snipets to force a re-render:
const forceUpdate = React.useState()[1].bind(null, {}) // see NOTE above
const forceUpdate = React.useReducer(() => ({}))[1]
You should check out his answer for a more detailed explanation.
Other sources include this answer to the same stack overflow question that references the official FAQ.
It solves the problem by doing:
const [ignored, forceUpdate] = useReducer(x => x + 1, 0);
Solving Your Specific Problem
I saw that you were able to solve your problem by using the useEffect hook - a great start for a potential solution. You also mentioned that you got an infinite loop while listening to a variable change in your hook - a common problem and one with some common solutions. In general, you should always run a check inside the useEffect hook before changing any of its dependencies. For example, run a check to see if the profile is unset before trying to update its value.
I however would recomend that you use a progress varible that would indicate your status, something like this:
const STATUS_START = 0;
const STATUS_LOGED_IN = 1;
const STATUS_SIGNING_UP = 2;
const [progress, setProgress] = useState(STATUS_START);
Then, you can simply listen to changes made to the progress variable in your useEffect hook (by passing it as your only dependent). This should automatically condition you to write the necessary logic to check for state inside of the useEffect function as I described previously.
This solution would work by initially setting the progress to either signing up or logging in, but only filling the form data if you are logged in (and after the signup progress is done calling setProgress(STATUS_LOGED_IN))
I have a react query to get user data like this
const { data: queryInfo, status: queryInfoLoading } = useQuery('users', () =>
getUsers()),
);
I then have a sibling component that needs the same data from the get users query. Is there a way to get the results of the get users query without re-running the query?
Essentially, I would like to do something like this
const userResults = dataFromUserQuery
const { data: newInfo, status: newInfoLoading } = useQuery('newUserData', () =>
getNewUsers(userResults.name)),
)
As suggested in this related question (how can i access my queries from react-query?), writing a custom hook and reusing it wherever you need the data is the recommended approach.
Per default, react-query will trigger a background refetch when a new subscriber mounts to keep the data in the cache up-to-date. You can set a staleTime on the query to tell the library how long some data is considered fresh. In that time, the data will always come from the cache if it exists and no refreshes will be triggered.
I'm still learning React and I'm trying to make a "design review app" where users signup as customers or designers and interact with each other.
I made the auth system and made sure that while signing up every user would get also some attributes in the firebase database.
Therefore, in my DB, I have a 'users/' path where every user is saved by uid.
Now I'm able to render a different dashboard if you're a customer or a designer.
In my customer dashboard, I just want to render a list of designers (and clicking on them go to their projects).
However, I'm having so many problems trying to get this stuff to work!
In the following code, I'm trying to fetch the users from the db and add their uid to an array.
Later I want to use this array and render the users with those uids.
import firebase from "firebase/app";
import "firebase/database";
export default function CustomerContent() {
const[designers, setDesigners] = useState([]);
function printUsers (){
var users = firebase.database().ref('/users/');
users.on('value', (snapshot)=>{
snapshot.forEach((user)=>{
console.log(user.key)
firebase.database().ref('/users/'+user.key).on('value', (snapshot)=>{
var role = snapshot.val().role
console.log(role)
if(role === 'designer'){
const newDesigners = [...designers, user.key];
setDesigners(newDesigners);
}
})
})
})
}
useEffect(() => {
printUsers();
console.log(designers);
}, [])
return (
<div>
designer list
</div>
)
}
Now the problem with this code is that:
it looks like it runs the printUsers functions two times when loading the page
the array is empty, however, if I link the function to a button(just to try it), it seems to add only 1 uid to the array, and always the same (I have no idea what's going on).
ps. the console.log(user.key) and the console.log(role) print the right user-role combination
It's not a stupid question. Here's what I'd change it to (of course you'd remove the console.logs later though). It's hard to know if this will work perfectly without having access to your database to run it, but based on my last react/firebase project, I believe it'll work.
The first thing was that you reference /users/, when you only need /users. I'm not sure if it makes a difference, but I did it the latter way and it worked for me.
Secondly, you're calling firebase more than you need to. You already have the information you need from the first time.
Third, and this is small, but I wouldn't call your function printUsers. You're doing more than just printing them- you're making a call to firebase (async) and you're setting the state, which are much larger things than just print some data to the console.
Lastly, I would store the entire object in your designers piece of state. Who knows what you'll want to display? Probably at least their name, then possibly their location, background, an icon, etc. You'll want all of that to be available in that array, and possibly you'll want to move that array into redux later if you're app is big enough.
I also added some JSX to the bottom that gives a simple output of what you could do with the designers array for the visual aspect of your app.
import firebase from 'firebase/app';
import 'firebase/database';
export default function CustomerContent() {
const [designers, setDesigners] = useState([]);
function printUsers() {
var users = firebase.database().ref('/users');
users.on('value', (snapshot) => {
snapshot.forEach((snap) => {
const userObject = snap.val();
console.log(userObject);
const role = userObject['role'];
console.log(role);
if (role === 'designer') {
const newDesigners = [...designers, userObject];
setDesigners(newDesigners);
}
});
});
}
useEffect(() => {
printUsers();
console.log(designers);
}, []);
return (
<div>
<h2>The designer are...</h2>
<ul>
{designers.map((designerObject) => {
return <li>{designerObject.name}</li>;
})}
</ul>
</div>
);
}
i'm new to react please forgive me if i'm asking a dumb question.
The idea is to access the tweets array from context, find the matching tweet and then set it in the component's state to access the data.
However, the tweets array results empty even though i'm sure it's populated with tweets
const { tweets } = useContext(TweeetterContext)
const [tweet, setTweet] = useState({})
useEffect(() => {
loadData(match.params.id, tweets)
}, [])
const loadData = (id, tweets) => {
return tweets.filter(tweet => tweet.id == id)
}
return (stuff)
}
You are accessing context perfectly fine, and it would be good if you could share a code where you set tweets.
Independent of that, potential problem I might spot here is related to the useEffect function. You are using variables from external context (match.params.id and tweets), but you are not setting them as dependencies. Because of that your useEffect would be run only once at the initial creation of component.
The actual problem might be that tweets are set after this initial creation (there is some delay for setting correct value to the tweets, for example because of the network request).
Try using it like this, and see if it fixes the issue:
useEffect(() => {
loadData(match.params.id, tweets)
}, [match.params.id, tweets])
Also, not sure what your useEffect is actually doing, as it's not assigning the result anywhere, but I'm going to assume it's just removed for code snippet clarity.
I have a collection of items in a Firebase Realtime database. Clients subscribe to modifications in the /items path of the database. But this has the effect of sending all items to the client each time a single item is added, updated or deleted. This could be up to 1000 items being sent to the client just because an item text has been updated with as little as one character.
This code works, but does not behave the way I want:
export const startSubscribeItems = () => {
return (dispatch, getState) => {
return new Promise(resolve => {
database.ref('items')
.orderByChild(`members/${uid}`)
.equalTo(true)
.on('value', (snapshot) => {
let items = []
snapshot.forEach( (childSnap) => {
const id = childSnap.key
const item = {id, ...childSnap.val()}
items.push(item)
})
dispatch(setItems(items))
resolve()
})
})
}
}
I wish to make this more network cost effective by only sending the item that has been updated - while keeping client subscriptions.
My initial thought was to implement a subscription for each item:
export const startSubscribeSingleItems = () => {
return (dispatch, getState) => {
return new Promise(resolve => {
database.ref('items')
.orderByChild(`access/members/${uid}`)
.equalTo(true)
.once('value', (snapshot) => {
let items = []
snapshot.forEach( (childSnap) => {
const id = childSnap.key
const item = {id, ...childSnap.val()}
items.push(item)
// .:: Subscribe to single item node ::.
database.ref(`items/${id}`).on('value', (snap)=>{
// Some logic here to handle updates and deletes (remove subscription)
})
})
dispatch(setItems(items))
resolve()
})
})
}
}
This seems a bit cumberstone, and only handles updates and deletes. It does not handle the case of additions made by another client. Additions would have to happen via a separate database node (eg. 'subscriptionAdditions//')? Also - initial load would have to clear all items in "subscriptionAdditions//" since first load reads all items.
Again, cumberstone. :/
In conclusion; Is there a simple and/or recommended way to achieve subscribing to single items while taking several clients into account?
Kind regards /K
Firebase Realtime Database synchronizes state between the JSON structure on the server, and the clients that are observing that state.
You seem to want to synchronize only a subset of that state, as far as I can see mostly about recent changes to the state. In that case, consider modeling the state changes themselves in your database.
As you work with NoSQL databases more, you'll see that is quite common to modify your data model to allow each use-case.
For example, if you only need the current state of nodes that have changed, you can add a lastUpdated timestamp property to each node. Then you can query for only the updates nodes with:
database.ref('items')
.orderByChild('lastUpdated')
.startAt(Date.now())
If you want to listen for changes since the client was last online, you'll want to store the timestamp that they were last online somewhere, and use that instead of Date.now().
If you want to synchronize all state changes, even if the same node was changed multiple times, you'll need to store each state change in the database. By keeping those with chronological keys (such as those generated by push()) or storing a timestamp for each, you can then use the same logic as before to only read state change that your client hasn't processed yet.
Also see:
NoSQL data modeling
How to only get new data without existing data from a Firebase?
Retrieve only childAdded from firebase to my listener in firebase