Repository: tannerlinsley/react-query
Pull request #3669 (2022-06-04 08:29:57)

fix: `replaceDeepEqual` special case for non-plain arrays
It turns out that the `replaceDeepEqual` function wasn't returning the intended value for objects that are non-plain arrays.

E.g.

```tsx
const prev = Object.assign([1, 2], { a: { b: 'b' }, c: 'c' })
const next = Object.assign([1, 2], { a: { b: 'b' }, c: 'c' })

replaceDeepEqual(prev, next) // result: [1, 2]
```

This data structure is used in libraries such as [ethers](https://docs.ethers.io/v5/) and their [contract `Result` value](https://docs.ethers.io/v5/api/utils/abi/interface/#Result).

I'm not sure if we should perform structural sharing on these values as they are not really "JSON-compatible" values as mentioned in the [docs](https://react-query.tanstack.com/guides/important-defaults), so I added a change which just checks if the given array is a "plain array", and if it is not, it has changed.

vercel[bot]: **The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Updated |
| :--- | :----- | :------ | :------ |
| **react-query** | 🔄 Building ([Inspect](https://vercel.com/tanstack/react-query/9ptpucn8aSJoHaejNdwos4eYwt6M)) |  | Jun 4, 2022 at 8:30AM (UTC) |


codesandbox-ci[bot]: This pull request is automatically built and testable in [CodeSandbox](https://codesandbox.io).

  To see build info of the built libraries, click [here](https://ci.codesandbox.io/status/tannerlinsley/react-query/pr/3669/builds/257384) or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 2a0533b33b09abc7e481e8e2e089dd9fda69b89a:

|Sandbox| Source |
|--|--|
|[tannerlinsley/react-query: basic](https://codesandbox.io/s/tannerlinsley-react-query-basic-vqmlol)| Configuration |
|[tannerlinsley/react-query: basic-typescript](https://codesandbox.io/s/tannerlinsley-react-query-basic-typescript-zm85en)| Configuration |


Patch:
diff --git a/src/core/infiniteQueryBehavior.ts b/src/core/infiniteQueryBehavior.ts
index df8f8f979..3cb416881 100644
--- a/src/core/infiniteQueryBehavior.ts
+++ b/src/core/infiniteQueryBehavior.ts
@@ -182,7 +182,7 @@ export function getPreviousPageParam(
  * Returns `undefined` if it cannot be determined.
  */
 export function hasNextPage(
-  options: QueryOptions<any, any>,
+  options: QueryOptions<any, any, any, any>,
   pages?: unknown
 ): boolean | undefined {
   if (options.getNextPageParam && Array.isArray(pages)) {
@@ -200,7 +200,7 @@ export function hasNextPage(
  * Returns `undefined` if it cannot be determined.
  */
 export function hasPreviousPage(
-  options: QueryOptions<any, any>,
+  options: QueryOptions<any, any, any, any>,
   pages?: unknown
 ): boolean | undefined {
   if (options.getPreviousPageParam && Array.isArray(pages)) {
diff --git a/src/core/infiniteQueryObserver.ts b/src/core/infiniteQueryObserver.ts
index 1622ed077..dbde391a9 100644
--- a/src/core/infiniteQueryObserver.ts
+++ b/src/core/infiniteQueryObserver.ts
@@ -5,6 +5,7 @@ import type {
   InfiniteData,
   InfiniteQueryObserverOptions,
   InfiniteQueryObserverResult,
+  QueryKey,
 } from './types'
 import type { QueryClient } from './queryClient'
 import {
@@ -27,12 +28,14 @@ export class InfiniteQueryObserver<
   TQueryFnData = unknown,
   TError = unknown,
   TData = TQueryFnData,
-  TQueryData = TQueryFnData
+  TQueryData = TQueryFnData,
+  TQueryKey extends QueryKey = QueryKey
 > extends QueryObserver<
   TQueryFnData,
   TError,
   InfiniteData<TData>,
-  InfiniteData<TQueryData>
+  InfiniteData<TQueryData>,
+  TQueryKey
 > {
   // Type override
   subscribe!: (
@@ -54,7 +57,8 @@ export class InfiniteQueryObserver<
       TQueryFnData,
       TError,
       TData,
-      TQueryData
+      TQueryData,
+      TQueryKey
     >
   ) {
     super(client, options)
@@ -71,7 +75,8 @@ export class InfiniteQueryObserver<
       TQueryFnData,
       TError,
       TData,
-      TQueryData
+      TQueryData,
+      TQueryKey
     >,
     notifyOptions?: NotifyOptions
   ): void {
@@ -89,7 +94,8 @@ export class InfiniteQueryObserver<
       TQueryFnData,
       TError,
       TData,
-      TQueryData
+      TQueryData,
+      TQueryKey
     >
   ): InfiniteQueryObserverResult<TData, TError> {
     options.behavior = infiniteQueryBehavior()
@@ -125,12 +131,13 @@ export class InfiniteQueryObserver<
   }
 
   protected createResult(
-    query: Query<TQueryFnData, TError, InfiniteData<TQueryData>>,
+    query: Query<TQueryFnData, TError, InfiniteData<TQueryData>, TQueryKey>,
     options: InfiniteQueryObserverOptions<
       TQueryFnData,
       TError,
       TData,
-      TQueryData
+      TQueryData,
+      TQueryKey
     >
   ): InfiniteQueryObserverResult<TData, TError> {
     const { state } = query
diff --git a/src/core/utils.ts b/src/core/utils.ts
index 8fdb107b5..cdec3d83f 100644
--- a/src/core/utils.ts
+++ b/src/core/utils.ts
@@ -314,7 +314,7 @@ export function replaceEqualDeep(a: any, b: any): any {
     return a
   }
 
-  const array = Array.isArray(a) && Array.isArray(b)
+  const array = isPlainArray(a) && isPlainArray(b)
 
   if (array || (isPlainObject(a) && isPlainObject(b))) {
     const aSize = array ? a.length : Object.keys(a).length
@@ -355,6 +355,10 @@ export function shallowEqualObjects<T>(a: T, b: T): boolean {
   return true
 }
 
+export function isPlainArray(value: unknown) {
+  return Array.isArray(value) && value.length === Object.keys(value).length
+}
+
 // Copied from: https://github.com/jonschlinkert/is-plain-object
 export function isPlainObject(o: any): o is Object {
   if (!hasObjectPrototype(o)) {


Test patch:
diff --git a/src/core/tests/utils.test.tsx b/src/core/tests/utils.test.tsx
index 1a0b8dd0d..6e00a9db0 100644
--- a/src/core/tests/utils.test.tsx
+++ b/src/core/tests/utils.test.tsx
@@ -6,6 +6,7 @@ import {
   matchMutation,
   scheduleMicrotask,
   sleep,
+  isPlainArray,
 } from '../utils'
 import { Mutation } from '../mutation'
 import { createQueryClient } from '../../tests/utils'
@@ -56,6 +57,16 @@ describe('core/utils', () => {
     })
   })
 
+  describe('isPlainArray', () => {
+    it('should return `true` for plain arrays', () => {
+      expect(isPlainArray([1, 2])).toEqual(true)
+    })
+
+    it('should return `false` for non plain arrays', () => {
+      expect(isPlainArray(Object.assign([1, 2], { a: 'b' }))).toEqual(false)
+    })
+  })
+
   describe('partialDeepEqual', () => {
     it('should return `true` if a includes b', () => {
       const a = { a: { b: 'b' }, c: 'c', d: [{ d: 'd ' }] }
@@ -258,6 +269,13 @@ describe('core/utils', () => {
       expect(result[1]).toBe(prev[1])
     })
 
+    it('should support objects which are not plain arrays', () => {
+      const prev = Object.assign([1, 2], { a: { b: 'b' }, c: 'c' })
+      const next = Object.assign([1, 2], { a: { b: 'b' }, c: 'c' })
+      const result = replaceEqualDeep(prev, next)
+      expect(result).toBe(next)
+    })
+
     it('should replace all parent objects if some nested value changes', () => {
       const prev = {
         todo: { id: '1', meta: { createdAt: 0 }, state: { done: false } },