React-Native loading state not updating when fetching data

Viewed 125

I've had this problem for a while and tried everything I could think of but so far I haven't found anything to solve this yet. When fetching data from my database I set my loading state to true and when I'm done I set it back to false, now the problem is that it never gets set to true. In the code example, I've delayed the fetch for 5 seconds so that it simulates slow internet to see the loading indicator and state change if it would work.

Edit: I'm using firebase firestore to get my data. to be more specific I use onsnapshot listener which is not asynchronous so I can't use async. I also removed the try-catch block since this has no use either, yet still, the loading state is not set to true before I call the database.

const [loading, setLoading] = useState(false);

useEffect(() => {
    setLoading(true);
    try {
      setTimeout(() => {
       //fetchting from database.
      }, 5000);
      return () => {
        /* unsubscribe(); */
      };
    } catch (error) {
      console.log(error);
    } finally {
      setLoading(false);
    }
  }, []);

Edited function:

const [loading, setLoading] = useState(false);

useEffect(() => {
    setLoading(true);
    setTimeout(() => {
      firebase.db
        .collection("collectionname")
        .onSnapshot((snapshot) => {
          //handeling data returned.
        })},
      5000
    );
    setLoading(false);
    return () => {
      /* unsubscribe(); */
    };
  }, []);
2 Answers

The problem is that the setLoading(false) will be called before the data (5 second wait) has ran. Here, you can await a Promise of a 5 second wait (in production you would use fetch or similar). Now, the finally block won't run until after the promise resolves.

const [loading, setLoading] = useState(true);

useEffect(() => {
  const getData = async () => {
    try {
      // get data (waiting 5 seconds for example)
      // note `await` - the `finally` won't run until after this has resolved
      await new Promise(res => setTimeout(res, 5000))
      return () => {
        // unsubscribe()
      }
    } catch (error) {
      console.error(error)
    } finally {
      setLoading(false)
    }
  }
  getData()
}, []);

Edit: Since you are getting the data through a callback, you need to wrap it in a promise so you can await it, like so:

useEffect(() => {
  const getData = async () => {
    try {
      const snapshot = await new Promise(resolve => 
        firebase.db
          .collection("collectionname")
          .onSnapshot(resolve)
      )
      return () => {
        // unsubscribe()
      }
    } catch (error) {
      console.error(error)
    } finally {
      setLoading(false)
    }
  }
  getData()
}, []);

How your code is executing is the important thing here. Your first setLoading runs and is set to true. Then, your setTimeout is initialized but (and here is where the problem is) immediately following the initialization of your setTimeout command, setLoading is called again and set to false because there is no break after the initalization of setTimeout. So, the loading is set to true before your db is called, but it is immediately set to false before anything has a chance to happen because of the execution order of your code.

Try setting your setLoading(false) within the setTimeout like so:

const [loading, setLoading] = useState(false);

useEffect(() => {
    setLoading(true);
    setTimeout(() => {
      firebase.db
        .collection("collectionname")
        .onSnapshot((snapshot) => {
          //handeling data returned.
        });

        //moved setLoading to within final setTimeout execution
        setLoading(false);
        },
      5000
    );
    
    return () => {
      /* unsubscribe(); */
    };
  }, []);
Related