uber
uber

Reputation: 5073

How to properly work with promises, Firebase and React's UseEffect hook?

Basically what I'm trying to achieve is to run some code only after a variable is not undefined anymore. However, I'm feeling very confused with the use of promises to await firebase responses, as well as with the use of React's useEffect hook.

I did some research that perhaps listeners are my answer to this? I couldn't really figure out how to implement them, though.

const weekday = moment().isoWeekday() 

export const TimeGrid = () => {
  const { userObject } = useContext(Context)

  //(1) I wish to use the below to generate a list of times
  const [LIST_OF_TIMES_FROM_DATABASE, SET_LIST_OF_TIMES_FROM_DATABASE] = useState([])

  let businessId

  function getBusinessId() {
    if (userObject) businessId = userObject.businessId
  }

  getBusinessId()

  //defines function that will run below inside of first useEffect
  //also where the list of times is supposed to be set
  function getWorkingHoursAccordingToDayOfTheWeek(day, snapshot) {
    const workingHours = []
    const ALL_AVAILABLE_TIMES = []

    workingHours.push(snapshot.data().beginWeek)
    workingHours.push(snapshot.data().endWeek)

    const [begin, ending] = workingHours
    let bgn = moment(begin, 'HH:mm')
    const end = moment(ending, 'HH:mm')
    let i = 0
    while (bgn <= end) {
      ALL_AVAILABLE_TIMES.push({
        id: i,
        type: 'time',
        day: 'today',
        time: bgn.format('HHmm'),
      })
      bgn = bgn.add(30, 'minutes')
      i++
    }
    SET_LIST_OF_TIMES_FROM_DATABASE(ALL_AVAILABLE_TIMES) //(2) This is where I set the list, which won't work on its 1st run
  }

  useEffect(() => {
    async function getTimesFromDB() {
      const approved = await approvedBusinessService.doc(businessId)
      const snapshotApproved = await approved.get()
      if (snapshotApproved.exists) {
        getWorkingHoursAccordingToDayOfTheWeek(weekday, snapshotApproved)
        return
      } else {
        const pending = await businessPendingApprovalService.doc(businessId)
        const snapshotPending = await pending.get()
        if (snapshotPending.exists) {
          getWorkingHoursAccordingToDayOfTheWeek(weekday, snapshotPending)
        }
      }
      return
    }

    getTimesFromDB()
  }, [userObject])

  const allBookedTimes = allBookings.map(element => element.time)
  //the below used to be 'listOfTimesJSON.map(item => item.time)' and used to work as it was a static JSON file
  //(3) this is sometimes undefined... so all of the code below which relies on this will not run
  const listOfTimes = LIST_OF_TIMES_FROM_DATABASE.map(item => item.time)

  const [counts, setCounts] = useState({})
  useEffect(() => {
    const counts = {}
    //(4) below will not work as listOfTimes won't exist (because of 'LIST_OF_TIMES_FROM_DATABASE' being undefined)
    listOfTimes.forEach(x => {
      counts[x] = (counts[x] || 0) + 1 

      if (allBookedTimes.includes(x)) counts[x] = counts[x] - 1
    })
    setCounts(counts)
  }, [])

  const sortedListOfTimesAndCount = Object.keys(counts).sort() //(5) undefined, because 'counts' was undefined.
  const displayListOfTimes = sortedListOfTimesAndCount.map(
    time =>
      time > currentTime && (
        <>
          <li>
            {time}
          </li>
        </>
      ),
  )

  return (
    <div>
      <ul>{displayListOfTimes}</ul>
    </div>
  )
}

Upvotes: 2

Views: 332

Answers (2)

Shubham Khatri
Shubham Khatri

Reputation: 281646

The problem is because you useEffect which uses listOfTimes isn't being called when the listOfTimes value updates after a fetch request.

You can define listOfTimes within the useEffect and add a dependency on LIST_OF_TIMES_FROM_DATABASE

  const [counts, setCounts] = useState({})
  useEffect(() => {
    // Defining it inside because if we don't and add allBookedTimes as a dependency then it will lead to an infinite look since references will change on each re-render

    const allBookedTimes = allBookings.map(element => element.time)
    const listOfTimes = LIST_OF_TIMES_FROM_DATABASE.map(item => item.time)
    const counts = {}
    listOfTimes.forEach(x => {
      counts[x] = (counts[x] || 0) + 1 

      if (allBookedTimes.includes(x)) counts[x] = counts[x] - 1
    })
    setCounts(counts)
  }, [LIST_OF_TIMES_FROM_DATABASE, allBookings]); // Add both dependencies. This will ensure that you update counts state when the state for times changes

Upvotes: 0

Vivek Doshi
Vivek Doshi

Reputation: 58543

as per the comments in your code your steps 3 ,4 ,5 are dependent on LIST_OF_TIMES_FROM_DATABASE and it's also not in sequence,

And you are expecting it to execute in the sequence,

1) First, it will execute all the thing which is outside the useEffect and it's dependency whenever there is change in state, so keep that in mind

2) useEffect(() => {...},[]) this will get called as soon as component mounts, so in most cases, you will get listOfTimes as [], because at that time API call might be in requesting data from server.

3) There are few unnecessary things like const [counts, setCounts] = useState({}) and useEffect(() => {...} , []), this is kind of overuse of functionality.

Sometimes you can do the things simply but with all these it becomes complicated


Solution :

So what you can do is just put all the code inside displayListOfTimes function and check for LIST_OF_TIMES_FROM_DATABASE if available then and only then proceed with steps 3,4,5. So you will never get LIST_OF_TIMES_FROM_DATABASE undefined

NOTE : And as per the code LIST_OF_TIMES_FROM_DATABASE either be blank array or array with data but never undefined, and if you are getting undefined then you can check that while setting up the state

const weekday = moment().isoWeekday() 

export const TimeGrid = () => {
  const { userObject } = useContext(Context)

  //(1) I wish to use the below to generate a list of times
  const [LIST_OF_TIMES_FROM_DATABASE, SET_LIST_OF_TIMES_FROM_DATABASE] = useState([])

  let businessId

  function getBusinessId() {
    if (userObject) businessId = userObject.businessId
  }

  getBusinessId()

  //defines function that will run below inside of first useEffect
  //also where the list of times is supposed to be set
  function getWorkingHoursAccordingToDayOfTheWeek(day, snapshot) {
    const workingHours = []
    const ALL_AVAILABLE_TIMES = []

    workingHours.push(snapshot.data().beginWeek)
    workingHours.push(snapshot.data().endWeek)

    const [begin, ending] = workingHours
    let bgn = moment(begin, 'HH:mm')
    const end = moment(ending, 'HH:mm')
    let i = 0
    while (bgn <= end) {
      ALL_AVAILABLE_TIMES.push({
        id: i,
        type: 'time',
        day: 'today',
        time: bgn.format('HHmm'),
      })
      bgn = bgn.add(30, 'minutes')
      i++
    }
    SET_LIST_OF_TIMES_FROM_DATABASE(ALL_AVAILABLE_TIMES) //(2) This is where I set the list, which won't work on its 1st run
  }

  useEffect(() => {
    async function getTimesFromDB() {
      const approved = await approvedBusinessService.doc(businessId)
      const snapshotApproved = await approved.get()
      if (snapshotApproved.exists) {
        getWorkingHoursAccordingToDayOfTheWeek(weekday, snapshotApproved)
        return
      } else {
        const pending = await businessPendingApprovalService.doc(businessId)
        const snapshotPending = await pending.get()
        if (snapshotPending.exists) {
          getWorkingHoursAccordingToDayOfTheWeek(weekday, snapshotPending)
        }
      }
      return
    }

    getTimesFromDB()
  }, [userObject])


  const displayListOfTimes = () => { // <----- Start - HERE
    if(LIST_OF_TIMES_FROM_DATABASE && LIST_OF_TIMES_FROM_DATABASE.length) {
      const counts = {}
      const allBookedTimes = allBookings.map(element => element.time)

      //(3) ------ this will never be undefined
      const listOfTimes = LIST_OF_TIMES_FROM_DATABASE.map(item => item.time)

      //(4) ------ now it will work always
      listOfTimes.forEach(x => {
        counts[x] = (counts[x] || 0) + 1 

        if (allBookedTimes.includes(x)) counts[x] = counts[x] - 1
      })

      //(5) ------ now it will work 
      const sortedListOfTimesAndCount = Object.keys(counts).sort() 
      return sortedListOfTimesAndCount.map(
        time =>
          time > currentTime && (
            <>
              <li>
                {time}
              </li>
            </>
          ),
      )
    } else {
      return <li>Nothing to show for now</li>
    }
  }

  return (
    <div>
      <ul>{displayListOfTimes}</ul>
    </div>
  )
}

Upvotes: 1

Related Questions