Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Double render issue when spreading query result #8612

Closed
hoangtrung99 opened this issue Feb 6, 2025 · 2 comments
Closed

Double render issue when spreading query result #8612

hoangtrung99 opened this issue Feb 6, 2025 · 2 comments

Comments

@hoangtrung99
Copy link

hoangtrung99 commented Feb 6, 2025

Describe the bug

Update sandbox
https://codesandbox.io/p/github/hoangtrung99/react-query-reproducible/main

Hello everyone,
Today, I encountered a very perplexing error that I cannot explain. I have a fairly simple pattern. The strange thing here is that when I don't use the spread operator on the result of useSuspenseQuery, the CustomerListInner component renders only once. However, when I spread the result of useSuspenseQuery, my CustomerListInner component renders twice. Even as shown in the video, merely spreading the result of useSuspenseQuery causes the component to render twice, even before using that result.

export const Customers = () => {
  return (
    <ListLayout>
      <Header />
      <Suspense fallback={<LoadingSkeleton />}>
        <CustomerList />
      </Suspense>
    </ListLayout>
  )
}
export const CustomerListInner = () => {
  // const keyword = useAtomValue(keywordAtom)
  const { data } = useGetCustomers()

  const { last_page: totalPages, data: customers } = data

  console.log(222, customers)

  return (
    <Stack gap={0} mt={{ base: 0, sm: 24 }}>
      <Stack className={listContainer}>
        {customers.map((customer) => (
          <CustomerItem key={customer.id} customerData={customer} />
        ))}
      </Stack>

      {!!totalPages && (
        <Center pt={32} pb={{ base: 32, sm: 0 }}>
          <Pagination total={totalPages} pageAtom={pageAtom} />
        </Center>
      )}
    </Stack>
  )
}

export const CustomerList = memo(CustomerListInner)
export const customersQueryOptions = (params: GetCustomersParams) =>
  queryOptions({
    queryKey: [LIST_CUSTOMERS_QUERY_KEY, params],
    queryFn: () => getCustomers(params),
    placeholderData: keepPreviousData,
    staleTime: 0
  })

export const useGetCustomers = () => {
  // const params = useAtomValue(customerParamsAtom)
  // console.log('params', params)
  return useSuspenseQueryDeferred(customersQueryOptions({ page: 1 }))
}
export function useSuspenseQueryDeferred<
  TQueryFnData = unknown,
  TError = DefaultError,
  TData = TQueryFnData,
  TQueryKey extends QueryKey = QueryKey
>(options: UseSuspenseQueryOptions<TQueryFnData, TError, TData, TQueryKey>) {
  const queryKey = useDeepCompareMemo(
    () => options.queryKey,
    [options.queryKey]
  )

  const deferredQueryKey = useDeferredValue(queryKey)

  const query = useSuspenseQuery({
    ...options,
    queryKey: deferredQueryKey
  })

  const isSuspending = queryKey !== deferredQueryKey

  // // add property without creating a new object
  // Object.defineProperty(query, 'isSuspending', {
  //   value: queryKey !== deferredQueryKey,
  //   enumerable: true
  // })

  // return query

  // it make render twice
  return { ...query, isSuspending }
 
}

Your minimal, reproducible example

Sorry, I cannot create a reproducible example right now.

Steps to reproduce

Screen.Recording.2025-02-06.at.15.08.02.mov

Expected behavior

  • Not render twice

How often does this bug happen?

None

Screenshots or Videos

No response

Platform

OS : MacOS
Browser: ARC
React Version: 19

Tanstack Query adapter

None

TanStack Query version

v5.66

TypeScript version

v5.7.2

Additional context

No response

@robingullo
Copy link

Tanstack Query tracks which fields of the query result you use and only rerenders when those fields change. When you spread the query result, you basically access all fields so the hook rerenders on any query state change (and there can be a lot!). You should probably return { query, isSuspending } instead of { ...query, isSuspending }. More on that feature here: https://tkdodo.eu/blog/react-query-render-optimizations#tracked-queries
There is also an ESlint rule to detect this mistake: https://tanstack.com/query/v5/docs/eslint/no-rest-destructuring

On a side note, you might not need useDeferredValue for your feature. With placeholderData: (previousData) => previousData, you can keep showing the value of the previous query while you fetch the new one: https://tanstack.com/query/latest/docs/framework/react/guides/placeholder-query-data#placeholder-data-as-a-function

@hoangtrung99
Copy link
Author

Tanstack Query tracks which fields of the query result you use and only rerenders when those fields change. When you spread the query result, you basically access all fields so the hook rerenders on any query state change (and there can be a lot!). You should probably return { query, isSuspending } instead of { ...query, isSuspending }. More on that feature here: tkdodo.eu/blog/react-query-render-optimizations#tracked-queries There is also an ESlint rule to detect this mistake: tanstack.com/query/v5/docs/eslint/no-rest-destructuring

On a side note, you might not need useDeferredValue for your feature. With placeholderData: (previousData) => previousData, you can keep showing the value of the previous query while you fetch the new one: tanstack.com/query/latest/docs/framework/react/guides/placeholder-query-data#placeholder-data-as-a-function

Oh, I see the problem now, thank you so much. @robingullo

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants