Reputation: 440
I am trying to do the following and cannot seem to get the most suitable solution: On the top level of my React app (App component) I am loading firebase and firestore. I have a react context where my auth info is stored in. The auth context is not immediately loaded but after some time. I am using the onAuthStateChanged (from firebase) to wait for it. My code looks as follows:
const firebase = new Firebase(); //custom class, instantiates firebase
const [authData, setAuthData] = useState({
firebase,
user: null,
isInitializing: true,
userdata: { info: null, data: null }
});
useEffect(() => {
// listen for auth state changes
const unsubscribe = firebase.auth.onAuthStateChanged(async returnedUser => {
//set Auth data into state to pass into context
setAuthData({ ...authData, user: returnedUser, isInitializing: false });
});
// unsubscribe to the listener when unmounting
return () => unsubscribe();
}, []);
Now I want to add a listener to this component to listen for profile data in firebase cloud firestore. They provide an onShapshot function for that.
Where to add that listener? Because I dont want to do too many calls. Can I safely place the listener in the unsubscribe function? How to make sure the listener ubsubscribes when unmounting?
First I tried with an additional useEffect hook, like this:
useEffect(() => {
if (authData.isInitializing || authData.user === null) return;
const ref = firebase.db.doc("/env/" + process.env.REACT_APP_FIRESTORE_ENVIRONMENT + "/users/" + authData.user.uid);
return ref.onSnapshot(doc => {
setAuthData({ ...authData, userinfo: doc.data() });
});
}, []);
This doesnt work because it only runs once (on mount), and then it just returns nothing because it is still initializing, and I cannot set it to run on every update because then it keeps querying firestore, updating state, and query (endless loop).
Passing down the state in the context works and I can use it anywhere (auth state). I just want to be able to have the basic user data in context. Now, I could add this manually (when user logs in, get the user profile + data and store in state), but I want to handle updates on this data as well, so I need the onSnapshot listener. How to make sure I call the listener only once?
Upvotes: 4
Views: 3834
Reputation: 10538
Passing down the state in the context works and I can use it anywhere (auth state). I just want to be able to have the basic user data in context. Now, I could add this manually (when user logs in, get the user profile + data and store in state), but I want to handle updates on this data as well, so I need the onSnapshot listener. How to make sure I call the listener only once?
It sounds like you want to utilise the array argument in React.useEffect()
. The second argument to React.useEffect()
is an array of dependencies. When any of those dependencies change, the effect will be executed again.
If you're using eslint, the react-hooks eslint plugin will highlight when you use hooks that capture variables in a closure but do not put those variables in the deps array.
The following change would ensure that onSnapshot
is only added once per component:
useEffect(() => {
if (authData.isInitializing || authData.user === null) return;
const ref = firebase.db.doc("/env/" + process.env.REACT_APP_FIRESTORE_ENVIRONMENT + "/users/" + authData.user.uid);
return ref.onSnapshot(doc => {
setAuthData({ ...authData, userinfo: doc.data() });
});
}, [authData.isInitializing, authData.user]);
The number of fetches executed would scale linearly with the number of components you have. If you anticipate this hook becoming used more than once, you may want to instead consider a model where the firebase.db.doc()
call is only called in one component, but many components can subscribe to the store. This is very similar to how React Redux works and I would recommend taking a look at their code to get an idea of how to accomplish that.
Oh and how to return (ubsubscribe listener) this way? Can I just do it as I wrote in my original second codeblock? So if isInitializing === true --> just return, and else return unsubscribe function?
Yes, that will work fine. There is no unsubscription function required unless onSnapshot()
is called.
Upvotes: 2