13

I use a lot of firestore snapshots in my react native application. I am also using React hooks. The code looks something like this:

useEffect(() => {
    someFirestoreAPICall().onSnapshot(snapshot => {

        // When the component initially loads, add all the loaded data to state.
        // When data changes on firestore, we receive that update here in this
        // callback and then update the UI based on current state

    });;
}, []);

At first I assumed useState would be the best hook to store and update the UI. However, based on the way my useEffect hook is set up with an empty dependency array, when the snapshot callback gets fired with updated data and I try to modify the current state with the new changes, the current state is undefined. I believe this is because of a closure. I am able to get around it using useRef with a forceUpdate() like so:

const dataRef = useRef(initialData);

const [, updateState] = React.useState();
const forceUpdate = useCallback(() => updateState({}), []);

useEffect(() => {
    someFirestoreAPICall().onSnapshot(snapshot => {

       // if snapshot data is added
       dataRef.current.push(newData)
       forceUpdate()

       // if snapshot data is updated
       dataRef.current.find(e => some condition) = updatedData
       forceUpdate()

    });;
}, []);

return(
// JSX that uses dataRef.current directly
)

My question is am I doing this correct by using useRef along with a forceUpdate instead of useState in a different way? It doesn't seem right that I'm having to update a useRef hook and call forceUpdate() all over my app. When trying useState I tried adding the state variable to the dependency array but ended up with an infinite loop. I only want the snapshot function to be initialized once and the stateful data in the component to be updated over time as things change on the backend (which fires in the onSnapshot callback).

Kevin Quiring
  • 639
  • 1
  • 11
  • 26

4 Answers4

29

It would be better if you combine useEffect and useState. UseEffect will setup and detach the listener, useState can just be responsible for the data you need.

const [data, setData] = useState([]);

useEffect(() => { 
       const unsubscribe = someFirestoreAPICall().onSnapshot(snap => {
         const data = snap.docs.map(doc => doc.data())
         this.setData(data)
       });

       //remember to unsubscribe from your realtime listener on unmount or you will create a memory leak
       return () => unsubscribe()
}, []);

Then you can just reference "data" from the useState hook in your app.

Michel K
  • 641
  • 1
  • 6
  • 18
Josh Pittman
  • 7,024
  • 7
  • 38
  • 66
  • 2
    So because this is a functional component I don't use the "this" keyword. Also, the problem is that `setData` isn't working the way it should inside of the snapshot listener. When I use `setData` inside the listener the data is lost on the next render of the component. So after initial load, the next time something changes on the backend and the listener fires, I want to look at and modify the current `data` but it's undefined even though I set it on initial load. The only way I have access to the data is if I use `useRef` which holds the `data` between renders. – Kevin Quiring Jan 29 '20 at 18:39
  • 3
    I think the issue is with `onSnapshot` listener itself w/ react hooks. If I setState inside a useEffect with the `, []` but not inside `onSnapshot`, things seem to work fine. – Kevin Quiring Jan 29 '20 at 18:42
  • Why do you need to use the "this" keyword? If you don't want the data to change don't use a real-time listener, you can use a simple get() call instead. – Josh Pittman Jan 30 '20 at 04:32
  • This worked for me. My function kept repeating over and over continuously. putting the return () => before evoking the function is what i was missing. It made all the difference. Thanks! – Michael Martell Jun 03 '22 at 03:23
2

I found that inside of the onSnapshot() method I was unable to access state(e.g. if I console.log(state) I would get an empty value.

Creating a helper function worked for, but I'm not sure if this is hack-y solution or not but something like:

[state, setState] = useState([])

stateHelperFunction = () => {
//update state here
setState()
}

firestoreAPICall.onSnapshot(snapshot => {
stateHelperFunction(doc.data())
})
Tepinvic
  • 33
  • 4
2

A simple useEffect worked for me, i don't need to create a helper function or anything of sorts,

useEffect(() => {
        const colRef = collection(db, "data")
        //real time update
        onSnapshot(colRef, (snapshot) => {
            snapshot.docs.forEach((doc) => {
                setTestData((prev) => [...prev, doc.data()])
                // console.log("onsnapshot", doc.data());
            })
        })
    }, [])
anshul
  • 661
  • 9
  • 27
  • 1
    This worked for me! I wasn't using `prev` which was the issue. – Kevin Quiring Feb 27 '22 at 18:00
  • 1
    So this answer partially helped me in that I'm able to access the current state while setting state by using `prev`. However, if I want to access the state not inside `setTestData`, but still within `onSnapshot`, the state is `undefined` in `onSnapshot` even though it is defined outside. It's like `onSnapshot` doesn't listen to state changes outside itself. – Kevin Quiring Feb 28 '22 at 21:56
-2

use can get the currentState using callback on set hook

const [state, setState] = useState([]);
firestoreAPICall.onSnapshot(snapshot => {
 setState(prevState => { prevState.push(doc.data()) return prevState; })
})

prevState will have Current State Value