When working with Firebase and React, in order to fetch data based on state changes or on inner database changes (from another user for example), I often rely on pieces of code like this one:
useEffect(() => {
const getGamesInSelectedGroup = () => {
if (!state.currentGroup) {
return
}
const db = getDatabase();
const resp = ref(db, `/games/${state.currentGroup.name}`);
onValue(resp, (snap) => {
if (snap.exists()) {
const data = snap.val()
const games = Object.keys(data).map(k => ({id: k, group: state.currentGroup.name, ...data[k]}))
setState((prev) => ({
...prev,
games: games,
isLoaded: true,
}));
return
}
setState((prev) => ({
...prev,
games: null,
isLoaded: true,
}));
toast.warning("no data for " + state.currentGroup.name)
})
}
getGamesInSelectedGroup();
}, [state.currentGroup])
However, I am wondering if, whenever state.currentGroup changes, a new listener to /games/${state.currentGroup.name} is created? If so, is there a mean to unsubscribe to previous listener before creating a new one?
I have thinking about replacing onValue by a get call, still conditioned on state.currentGroup and using onValue outside useEffectto reflect "inner" database change.
Rather than nest everything in your getGamesInSelectedGroup function (which is the pattern you would use for Promise-based APIs), just call it in the body of the useEffect to make managing the listener simpler:
useEffect(() => {
if (!state.currentGroup) {
return
}
const db = getDatabase();
const resp = ref(db, `/games/${state.currentGroup.name}`);
return onValue(resp, (snap) => { // <--- return the unsubscriber!
if (snap.exists()) {
const data = snap.val()
const games = Object.keys(data)
.map(k => ({
id: k,
group: state.currentGroup.name,
...data[k]
}));
setState((prev) => ({
...prev,
games, // you can use this instead of "games: games"
isLoaded: true,
}));
return
}
setState((prev) => ({
...prev,
games: null,
isLoaded: true,
}));
toast.warning("no data for " + state.currentGroup.name)
});
}, [state.currentGroup])
I would also recommend using a "snapshot to array of children" function rather than using Object.keys(snapshot.val()) to maintain the sort order from the query (it would be ignored using the code as-is). Unfortunately, at the time of writing, there is no equivalent of Firestore's QuerySnapshot#docs for the RTDB just yet. But it's pretty easy to make our own:
// returns array of DataSnapshot objects under this DataSnapshot
// put this outside of your component, like in a common function library file
const getSnapshotChildren = (snapshot) => {
const children = [];
// note: the curly braces on the next line are important! If the
// callback returns a truthy value, forEach will stop iterating
snapshot.forEach(child => { children.push(child) })
return children;
}
useEffect(() => {
if (!state.currentGroup) {
return
}
const db = getDatabase();
const resp = ref(db, `/games/${state.currentGroup.name}`);
return onValue(resp, (snap) => { // <--- return the unsubscriber!
if (snap.exists()) {
const games = getSnapshotChildren(snap)
.map(child => ({
id: child.key,
group: state.currentGroup.name,
...child.val()
}));
setState((prev) => ({
...prev,
games, // you can use this instead of "games: games"
isLoaded: true,
}));
return
}
setState((prev) => ({
...prev,
games: null,
isLoaded: true,
}));
toast.warning("no data for " + state.currentGroup.name)
});
}, [state.currentGroup])