Home > Enterprise >  react-query useQuery skipping entries when fetching asynchronously
react-query useQuery skipping entries when fetching asynchronously

Time:12-27

In a high level overview I am building a tinder like app functionality. Where in my example, 4 entries are fetched from my DB, the user is shown one at a time and can click a like or a dislike button. Each button click triggers some asynchronous functions for writing the event to my DB. Once the last entry that was fetched is clicked I need to go and fetch the next 4 entries in my DB.

MY COMPONENT

export const HomePage: React.FC = () => {
  const { user } = useUser()
  const userId = user?.id
  const [current, setCurrent] = useState<number | null>(0)
  const [skip, setSkip] = useState(0)

  const [url, setUrl] = useState(
    `/api/swipes/get-users/?userId=${user?.id}&skip=${skip}`
  )

  const getUsers = async (url: string) => {
    const { data } = await axios.get(url)
    return data
  }

  const { error, data, isLoading, refetch } = useQuery(
    ['userSwipeData', url],
    () => getUsers(url),
    {
      enabled: !!user?.id,
    }
  )

  const handleRefetchUsers = () => {
    setCurrent(0)
    setUrl(
      `/api/swipes/get-users/?userId=${user?.id}&skip=${
        skip   FETCH_USERS_PAGINATION_LIMIT
      }`
    )
    refetch()
    setSkip(skip   FETCH_USERS_PAGINATION_LIMIT)
  }

  const handleSwipe = async (e: any) => {
    if (current === null || !data) return

    const { value } = e.target
    await handleSwipeType(value, data?.users[current].id)
  }

  const handleSwipeType = async (type: string, id: string) => {
    const values = {
      userSwipeOn: id,
      currentUser: userId,
    }

    // if (type === 'YES') {
    //   if (current && data?.users[current]?.isMatch) {
    //     await axios.post('/api/swipes/create-match', values)
    //     alert('You have a match!')
    //   }

    //   await axios.post('/api/swipes/like', values)
    // } else {
    //   await axios.post('/api/swipes/dislike', values)
    // }
    if (current === data.users.length - 1) {
      handleRefetchUsers()
    } else {
      setCurrent(current!   1)
    }
  }

  if (isLoading || current === null) return <Box>Fetching users...</Box>
  if (error) return <Box>An error has occurred </Box>
  if (data && !data?.users.length) return <Box>No more users for now</Box>

  return (
    <Box>
      <h1>Current User: {data?.users[current].email}</h1>
      <Flex>
        <button value="NO" onClick={handleSwipe}>
          NO
        </button>
        <button value="YES" onClick={handleSwipe}>
          YES
        </button>
      </Flex>
    </Box>
  )
}

The problem I am facing is that in this current state when the handleRefetchUsers() function triggers it works as expected. However, if I am to uncomment all the asynchronous code which I need to run on every click to document the event, once the handleRefetchUsers() trigger I notice it is skipping 4 entries every time it runs. I'm really at a loss as to why because the check for a final entry should only run after the async code has finished. Any ideas would be helpful.

CodePudding user response:

I'm pretty sure that refetch doesn't wait for setUrl to actually update the url

You shouldn't really base one state on another state

To fix it I would replace

const [url, setUrl] = useState(
  `/api/swipes/get-users/?userId=${user?.id}&skip=${skip}`
)

with

const url = /api/swipes/get-users/?userId=${user?.id}&skip=${skip}`

and remove refetch entirely. react-query will refetch anyway because the url changed

CodePudding user response:

There are a few things that could be improved, but your main issue is that setState is async. So when you use setUrl and then call refetch, refetch is still looking at the old url value.

I think a cleaner way would be to use refetch inside an effect, that has current and skip in the dependency array.

Also, url is a derived state, so it doesn't really need its own state. And you should also be using an arrow function when a new state relies on the previous state - again because setState is async, and it's possible that you are referencing an old state.

const buildUrl = (user, skip) => user?.id ? `/api/swipes/get-users/?userId=${user?.id}&skip=${skip}` : ''

export const HomePage: React.FC = () => {
  const { user } = useUser()
  const userId = user?.id

  const [current, setCurrent] = useState<number>(0) // current page
  const [skip, setSkip] = useState(0)

  const getUsers = async (url: string) => {
    const { data } = await axios.get(url)
    return data
  }

  const { error, data, isLoading, refetch } = useQuery(
    ['userSwipeData', buildUrl(user, skip)],
    () => getUsers(buildUrl(user, skip)),
    {
      enabled: !!user?.id,
    }
  )

  const handleRefetchUsers = () => {
    setCurrent(0)
    setSkip((prev) => prev   FETCH_USERS_PAGINATION_LIMIT)
  }

  const handleSwipe = async (e: any) => {
    if (current === null || !data) return
    const { value } = e.target
    await handleSwipeType(value, data?.users[current].id)
  }

  const handleSwipeType = async (type: string, id: string) => {
    const values = {
      userSwipeOn: id,
      currentUser: userId,
    }

    if (type === 'YES') {
      if (current && data?.users[current]?.isMatch) {
        await axios.post('/api/swipes/create-match', values)
        alert('You have a match!')
      }
      await axios.post('/api/swipes/like', values)
    } else {
      await axios.post('/api/swipes/dislike', values)
    }

    if (current === data.users.length - 1) {
      handleRefetchUsers()
    } else {
      setCurrent((prev) => prev   1)
    }
  }

  useEffect(() => {
    refetch()
  }, [current, skip])

  if (isLoading || current === null) return <Box>Fetching users...</Box>
  if (error) return <Box>An error has occurred </Box>
  if (data && !data?.users.length) return <Box>No more users for now</Box>

  return (
    <Box>
      <h1>Current User: {data?.users[current].email}</h1>
      <Flex>
        <button value="NO" onClick={handleSwipe}>
          NO
        </button>
        <button value="YES" onClick={handleSwipe}>
          YES
        </button>
      </Flex>
    </Box>
  )
}
  • Related