0
votes

Context: I have a custom hook that fetches firestore documents. It watches for document changes, then returns it and the loading state. I'm using this hook in another component.

Problem: Changes to the state values returned by the hook (loading and document) do not trigger the component using the hook to rerender and see the updated values.

My Findings: Other custom hooks work and trigger renders, so there must be something wrong with the useDocument hook. If another hook triggers a rerender, only then will loading finally update. The values of loading and document indeed update as expected within the hook, but a rerender of the component is still never triggered.

Important Notes: useFirebase() is a hook called within useDocument whose state is lifted up into context using the constate library. So technically this hook is a consumer of some provider higher up the tree that keeps track of the firebase libraries. This is to avoid unnecessary network calls.


export function useDocument() {
  const { auth, firestore } = useFirebase() // asynchronously fetches firebase modules
  const [loading, setLoading] = useState(true)
  const [document, setDocument] = useState()

  let unsubscribe = null

  

  useEffect(() => {
    let unsubscribe = null
    const watchDocument = () =>
      params?.query.onSnapshot(
        (doc) => {
          setDocument(doc.data() as T | undefined)
          setLoading(false)
        },
        (err) => {
          console.log(err)

          setError(err.message)
        }
      )
    if (firestore && auth?.currentUser) {
      unsubscribe = watchDocument()
    }
    return () => {
      unsubscribe && unsubscribe()
    }
  }, [firestore, auth?.currentUser])

  return {
    document,
    loading
  }
}

const MyComponent = () => {
  const {document, loading} = useDocument()
  console.log(loading) // changes to "loading" doesn't trigger rerender of this component
}
2

2 Answers

1
votes

There are few mistakes in this snippet, the main one is that the variable unsubscribe is always undefined as its not even in the scope of the useEffect.

This might be a valid fix (never used firebase, just by looking at the code):

export function useDocument() {
  const { auth, firestore } = useFirebase();
  const [loading, setLoading] = useState(true);
  const [document, setDocument] = useState();

  useEffect(() => {
    const watchDocument = async () => {
      const unsubscribe = await params?.query.onSnapshot(
        (doc) => {
          setDocument(doc.data());
          setLoading(false);
        },
        (err) => {
          console.log(err);
          setError(err.message);
        }
      );
    };

    let unsubscribe = null;
    if (firestore && auth) {
      unsubsribe = watchDocument();
    }
    return () => {
      unsubscribe?.();
    };
  }, [firestore, auth]);

  return {
    document,
    loading,
  };
}

const MyComponent = () => {
  const { document, loading } = useDocument();
  console.log(loading);
};
0
votes

So the issue is the dependency auth?.currentUser. auth itself properly triggers a re-render when state changes. The problem seemed to be that changes in auth?.currentUser is not detectable.

My assumption is that this is because auth is what is being tracked, and the reference to auth doesn't change even when currentUser is updated by the firebase library. I ended up moving currentUser into its own useState, and now the component properly rerenders.