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.
CodePudding user response:
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])
