6

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.

Doug Stevenson
  • 297,357
  • 32
  • 422
  • 441
jack_oneill
  • 63
  • 1
  • 4
  • I think the reason why it's being merged is that you only create the `fetchedTasks` array once onMount. After that, every time you say `fetchedTasks.push(..)` you add to the same array. So the list just gets longer and longer. You probably want to either reset `fetchedTasks ` every time you get the values, or, use `.map` instead of `forEach` and then call `setTasks` with the return value of `.map` – Daniel Nov 05 '19 at 10:47
  • Thanks so much! That actually solved my problem! You gave me a great hint. I looked at my code once again and quickly realized that `fetchedTasks` should be declared directly in a snapshot callback function. Then I put `props.firebase.db` in dependency list. Everything updated and no performance issues! – jack_oneill Nov 05 '19 at 11:25

1 Answers1

8

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.

Daniel
  • 525
  • 3
  • 10