TanStack / query

🤖 Powerful asynchronous state management, server-state utilities and data fetching for the web. TS/JS, React Query, Solid Query, Svelte Query and Vue Query.
https://tanstack.com/query
MIT License
42.23k stars 2.87k forks source link

How to use useInfiniteQuery with custom props #307

Closed sbogdaniuk closed 4 years ago

sbogdaniuk commented 4 years ago

Let's picture situation where we have filters with infinite scroll data view.

So I have have code like this:

// api
const queryPosts = async (props = {}) => {
  const { sort, page } = props
  const res = await axios.get(`/posts?sort=${sort}&page=${page}`)
  return {
    items: res.data,
    page,
  }
}

// Posts
const [sort, setSort] = React.useState('views')
const payload = useInfiniteQuery(
  ['infinite-posts', { sort, page: 1 }],
  queryPosts,
  {
    getFetchMore: lastGroup => {
      const { items, group } = lastGroup
      if (items.length) {
        return { page: page + 1, sort }
      }
      return false
    }
  }
)

At first it triggers correct, but when I call () => fetchMore() sort is disappeared, and page remains as initial.

Maybe I'm doing something wrong?

AvraamMavridis commented 4 years ago

Unfortunately I have the same issue, is not clear from the docs how to use it :(

tannerlinsley commented 4 years ago

The issue here is that you are not mapping up your query key to your query function's arguments properly. Each item in the query key is passed to the query function, including infinite-posts in your case. The next issue is that you're not accounting that the result from getFetchMore is passed as an optional parameter to your query function, which is why on the first request, you must provide a default value.

async function queryPosts (key, sort, page = 1) {
  // key === 'infinite-posts'
  // sort will always be there from the query-key
  // page information won't be there on the first request
  // which is why is has a defualt value of 1, but it will be
  // there on subsequent requests
  const res = await axios.get(`/posts?sort=${sort}&page=${page}`)

  return {
    items: res.data,
    page,
  }
}

const queryInfo = useInfiniteQuery(['infinite-posts', sort], queryPosts, {
  getFetchMore: ({ items, page }) => {
    if (items.length) {
      return page + 1 // This will be sent as the LAST parameter to your query function
    }

    return false
  },
})
AvraamMavridis commented 4 years ago

@tannerlinsley what is the proper way to update the sort in your example? are the queryPost automatically updated when sort changes?

tannerlinsley commented 4 years ago

Yep! You can update it however you'd like (via state, props, etc). Since it's part of the query key, it will automatically trigger a new refetch when it changes.

AvraamMavridis commented 4 years ago

@tannerlinsley thx a lot for the help. I loved the lib, for me is the missing abstraction for the hooks + fetching.

tannerlinsley commented 4 years ago

:) You're welcome! Thanks for using it! You should consider writing a blog post on that thought. It sounds like a great prompt: "React Query: The missing abstraction for hooks and data fetching"

sbogdaniuk commented 4 years ago

@tannerlinsley thanks it works.

One more question: Is there a possibility to change in queryPosts (key, sort, page = 1) second parameter via fetchMore? As I can see fetchMore changing only third parameter

tannerlinsley commented 4 years ago

You would have to do that override in the query function itself and use an optional override sort if it's there from the fetchMore

ralrom commented 4 years ago

Hello, I think this could be better documented. I also had trouble using useInfiniteQuery with an additional parameter until I found this issue.

qkreltms commented 4 years ago

I absolute agree with @ralrom 's ideas

rsarai commented 3 years ago

For anyone facing a facet of this issue, the following worked for me:

async function queryPosts (queryKey, pageParam = 1) {
  const sort = queryKey[1];  // queryKey[0] is the original query key 'infinite-posts'
  const res = await axios.get(`/posts?sort=${sort}&page=${pageParam}`)

  return {
    items: res.data,
    page,
  }
}

const queryInfo = useInfiniteQuery(['infinite-posts', sort], queryPosts, {
  getFetchMore: ({ items, page }) => { // edit: this was replaced by getNextPageParam
    if (items.length) {
      return page + 1 // This will be sent as the LAST parameter to your query function
    }

    return false
  },
})
Alecell commented 3 years ago

@rsarai maybe this isn't a good way to to send the props to the service since it forces we to write the API request with some boilerplate args or the args as object, probably breaking the re-usability of that service outside of react-query on some cases. Another point is that as I see on the docs neither the useInfiniteQuery or useQuery accepts getFetchMore method on react-query v3.

But as I found here, on the migration guide have a really useful doc about useInfiniteQuery args.

Basically we just need to call the service through a function like this:

// Old
 useInfiniteQuery(['posts'], (_key, pageParam = 0) => fetchPosts(pageParam))

 // New
 useInfiniteQuery(['posts'], ({ pageParam = 0 }) => fetchPosts(pageParam))

with this we can treat the pageParams isolated from the service and, sending an object to fetchNextPage we get the new props of the function, it becomes really useful if we create a custom hook as below. But is good to say, I have to work on a more readable way to write this, but at least IMO this is better than change the way we write our services here.

 const useMyRequest = (param1, param2) => {
  useInfiniteQuery(
    'posts', 
    ({ pageParam = 0 }) => {
      const obj = {
        param1: pageParam.param1 || param1,
        param2: pageParam.param2 || param2
      }

      return fetchPosts(obj.param1, obj.param2)
    }
  )
}

BTW I'm pretty new to react-query and I'm probably missing something, @tannerlinsley what you think about that approach?

rsarai commented 3 years ago

hey @Alecell, greetings, noticed we share the same timezone. You are 100% correct about the getFetchMore, on my project I'm actually using getNextPageParam (:see_no_evil:).

I was trying to go for different types of query keys. I'm also new to react-query so I'm happy to hear other comments.

mnadeem commented 3 years ago

Following worked for me

export const getLookupDefsSorted = async ({queryKey, pageParam = 0}) => {
  const sortBy = queryKey[1];  // queryKey[0] is the original query key 'infiniteLookupDefs'
  const sortDirection = queryKey[2]; 
  const url = `${URL_LOOKUP_DEF}/?pageNumber=${pageParam}&pageSize=${LOOKUP_DEF_PAGE_SIZE}&sortBy=${encodeURIComponent(sortBy)}&sortDirection=${encodeURIComponent(sortDirection)}`;
  const res = await axios.get(url)
  return res.data
}

export const useInfiniteQueryLookupDefsSorted = (sortBy, sortDirection) => {
  return useInfiniteQuery([QUERY_NAME_LOOKUP_DEFS_INFINITE, sortBy, sortDirection], getLookupDefsSorted, {
      getNextPageParam: (lastPage, allPages) => lastPage.last ? null : lastPage.number + 1,
      staleTime: 5 * 60 * 1000, // 5 minutes
  });
}
kasongoyo commented 11 months ago

What worked for me and I can any number of extra params as I want

export const useGetProducts = (filters: GetProductsQueryParamsType) =>
  useInfiniteQuery({
    queryKey: productKeys.listStoreProducts(filters),
    queryFn: async ({ pageParam = 1 }) => getProducts({ ...filters, page: pageParam }),
    getNextPageParam: (lastPage) => {
      const newPage = lastPage.page + 1
      const numPages = Math.ceil(lastPage.total / lastPage.take)
      if (newPage <= numPages) {
        return newPage
      } else {
        return lastPage.page
      }
    }
  });

Request

export async function getProducts({page, ...otherParams}) {
  return axiosInstance
    .get(`/products`, { params: {page, ...otherParams} })
    .then(...)
    .catch(...);
}