Home > Software engineering >  custom hooks return value doesn't change in Component
custom hooks return value doesn't change in Component

Time:02-26

My custom hook fetches data asynchronously. When it is used in a component, returned value doesn't get updated. It keeps showing default value. Does anybody know what is going on? Thank you!

import React, {useState, useEffect} from 'react'
import { getDoc, getDocs, Query, DocumentReference, deleteDoc} from 'firebase/firestore'

export const useFirestoreDocument = <T>(docRef: DocumentReference<T>) => {
  const [value, setValue] = useState<T|undefined>(undefined)
  const [isLoading, setIsLoading] = useState<boolean>(true)
  const update = async () => {
    const docSnap = await getDoc(docRef)
    if (docSnap.exists()) {
      const data = docSnap.data()
      setValue(data)
    }
    setIsLoading(false)
  }
  useEffect(() => {
    update()
  }, [])
  console.log(value, isLoading)  // it can shows correct data after fetching
  return {value, isLoading}
}
import { useParams } from 'react-router-dom'
const MyComponent = () => {
  const {userId} = useParams()
  const docRef = doc(db, 'users', userId!)
  const {value, isLoading} = useFirestoreDocument(docRef)
  console.log(value, isLoading)  // keeps showing {undefined, true}.
  return (
    <div>
    ...
    </div>
  )
}

CodePudding user response:

It looks like youe hook is only being executed once upon rendering, because it is missing the docRef as a dependency:

export const useFirestoreDocument = <T>(docRef: DocumentReference<T>) => {
  const [value, setValue] = useState<T|undefined>(undefined)
  const [isLoading, setIsLoading] = useState<boolean>(true)
 
  useEffect(() => {
    const update = async () => {
      const docSnap = await getDoc(docRef)
      if (docSnap.exists()) {
        const data = docSnap.data()
        setValue(data)
      }
      setIsLoading(false)
    }
    update()
  }, [docRef])
  console.log(value, isLoading)  // it can shows correct data after fetching
  return {value, isLoading}
}

In addition: put your update function definition inside the useEffect hook, if you do not need it anywhere else. Your linter will complaing about the exhaustive-deps rule otherwise.

CodePudding user response:

The useEffect hook is missing a dependency on the docRef:

export const useFirestoreDocument = <T>(docRef: DocumentReference<T>) => {
  const [value, setValue] = useState<T|undefined>(undefined);
  const [isLoading, setIsLoading] = useState<boolean>(true);
  
  useEffect(() => {
    const update = async () => {
      setIsLoading(true);
      try {
        const docSnap = await getDoc(docRef);
        if (docSnap.exists()) {
          const data = docSnap.data();
          setValue(data);
        }
      } catch(error) {
        // handle any errors, log, etc...
      }
      setIsLoading(false);
    };

    update();
  }, [docRef]);

  return { value, isLoading };
};

The render looping issue is because docRef is redeclared each render cycle in MyComponent. You should memoize this value so a stable reference is passed to the useFirestoreDocument hook.

const MyComponent = () => {
  const {userId} = useParams();

  const docRef = useMemo(() => doc(db, 'users', userId!), [userId]);

  const {value, isLoading} = useFirestoreDocument(docRef);

  console.log(value, isLoading);
  return (
    <div>
    ...
    </div>
  );
};
  • Related