Home > Software engineering >  Mix useEffect and Firebase's onValue
Mix useEffect and Firebase's onValue

Time:02-07

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])
  •  Tags:  
  • Related