Ad

How To Properly Update State With Firebase And UseEffect()?

I'm trying to fetch data from Firebase and then push it into my state. The goal is to create a mechanism that will do that every time data in Firebase changes.

I used on() method along with useEffect(). Unfortunately, React is not re-rendering my component even when the state changes. (ignore direct reference, it's just for testing)

const [tasks, setTasks] = useState([]);

    useEffect(() => {
        const fetchedTasks = [];
        const ref = props.firebase.db.ref('users/1zfRCHmD4MVjJj7L884LL4TMwAH3');
        const listener = ref.on('value', snapshot => {
            snapshot.forEach(childSnapshot => {
                const key = childSnapshot.key;
                const data = childSnapshot.val();
                fetchedTasks.push({ id: key, ...data });
            });
            setTasks(fetchedTasks);
        });
        return () => ref.off('value', listener);
    }, []);

At this point, when I change my data manually in Firebase Console, I can see in my Dev Tools that it triggers the listener, data is fetched, but it's merged with the previous state (tasks).

I want it to replace the previous state of course. Secondly, the component is not re-rendering. I know that the empty dependency list is responsible for that ("[]") and the component mounts only once, but when I remove the list, the component is updating and quickly freezes my browser. I also tried "[tasks]" and the result is similar - the component is re-rendering over and over again. Firebase is provided by the context and ESLint displays this:

"React Hook useEffect has a missing dependency: 'props.firebase.db'. Either include it or remove the dependency array."

When I do this, it's still not working and the component isn't updating.

Ad

Answer

As my comment solved the issue, here's the official answer:

The reason your state is always merged, is that your variable fetchedTasks is only declared once when the hook useEffect runs onMount. After that, every time your firebase database updates new values and you call fetchedTasks.push(...), you are pushing on the same "old" array. Therefore your list is getting longer and longer.

One solution would be to just re-declare fetchedTasks, or set it back to an empty arry.

const [tasks, setTasks] = useState([]);

    useEffect(() => {
        const ref = props.firebase.db.ref('users/1zfRCHmD4MVjJj7L884LL4TMwAH3');
        const listener = ref.on('value', snapshot => {
            const fetchedTasks = [];
            snapshot.forEach(childSnapshot => {
                const key = childSnapshot.key;
                const data = childSnapshot.val();
                fetchedTasks.push({ id: key, ...data });
            });
            setTasks(fetchedTasks);
        });
        return () => ref.off('value', listener);
    }, [props.firebase.db]);

Another one would be to use .map instead, and use the return value for setTasks. I think this would be my preferred one:

const [tasks, setTasks] = useState([]);

    useEffect(() => {
        const ref = props.firebase.db.ref('users/1zfRCHmD4MVjJj7L884LL4TMwAH3');
        const listener = ref.on('value', snapshot => {
            const fetchedTasks = snapshot.map(childSnapshot => {
                const key = childSnapshot.key;
                const data = childSnapshot.val();
                return { id: key, ...data };
            });
            setTasks(fetchedTasks);
        });
        return () => ref.off('value', listener);
    }, [props.firebase.db]);

aso note, that you'd have to pass props.firebase.db to your dependency array, to get rid of the eslint warning. The reference to props.firebase.db should never change, so it should be save to declare it in the dependency array.

Ad
source: stackoverflow.com
Ad