Skip to content

Commit 71c3c8c

Browse files
authored
Merge pull request #3193 from reduxjs/bugfix/3147-query-args-perf
2 parents 4fba13e + 04f4131 commit 71c3c8c

File tree

2 files changed

+93
-10
lines changed

2 files changed

+93
-10
lines changed

packages/toolkit/src/query/defaultSerializeQueryArgs.ts

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,38 @@ import type { QueryCacheKey } from './core/apiState'
22
import type { EndpointDefinition } from './endpointDefinitions'
33
import { isPlainObject } from '@reduxjs/toolkit'
44

5+
const cache: WeakMap<any, string> | undefined = WeakMap
6+
? new WeakMap()
7+
: undefined
8+
59
export const defaultSerializeQueryArgs: SerializeQueryArgs<any> = ({
610
endpointName,
711
queryArgs,
812
}) => {
13+
let serialized = ''
14+
15+
const cached = cache?.get(queryArgs)
16+
17+
if (typeof cached === 'string') {
18+
serialized = cached
19+
} else {
20+
const stringified = JSON.stringify(queryArgs, (key, value) =>
21+
isPlainObject(value)
22+
? Object.keys(value)
23+
.sort()
24+
.reduce<any>((acc, key) => {
25+
acc[key] = (value as any)[key]
26+
return acc
27+
}, {})
28+
: value
29+
)
30+
if (isPlainObject(queryArgs)) {
31+
cache?.set(queryArgs, stringified)
32+
}
33+
serialized = stringified
34+
}
935
// Sort the object keys before stringifying, to prevent useQuery({ a: 1, b: 2 }) having a different cache key than useQuery({ b: 2, a: 1 })
10-
return `${endpointName}(${JSON.stringify(queryArgs, (key, value) =>
11-
isPlainObject(value)
12-
? Object.keys(value)
13-
.sort()
14-
.reduce<any>((acc, key) => {
15-
acc[key] = (value as any)[key]
16-
return acc
17-
}, {})
18-
: value
19-
)})`
36+
return `${endpointName}(${serialized})`
2037
}
2138

2239
export type SerializeQueryArgs<QueryArgs, ReturnType = string> = (_: {

packages/toolkit/src/query/tests/defaultSerializeQueryArgs.test.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,69 @@ test('nested object arg is sorted recursively', () => {
4444
`"test({\\"age\\":5,\\"name\\":{\\"first\\":\\"Banana\\",\\"last\\":\\"Split\\"}})"`
4545
)
4646
})
47+
48+
test('Fully serializes a deeply nested object', () => {
49+
const nestedObj = {
50+
a: {
51+
a1: {
52+
a11: {
53+
a111: 1,
54+
},
55+
},
56+
},
57+
b: {
58+
b2: {
59+
b21: 3,
60+
},
61+
b1: {
62+
b11: 2,
63+
},
64+
},
65+
}
66+
67+
const res = defaultSerializeQueryArgs({
68+
endpointDefinition,
69+
endpointName,
70+
queryArgs: nestedObj,
71+
})
72+
expect(res).toMatchInlineSnapshot(
73+
`"test({\\"a\\":{\\"a1\\":{\\"a11\\":{\\"a111\\":1}}},\\"b\\":{\\"b1\\":{\\"b11\\":2},\\"b2\\":{\\"b21\\":3}}})"`
74+
)
75+
})
76+
77+
test('Caches results for plain objects', () => {
78+
const testData = Array.from({ length: 10000 }).map((_, i) => {
79+
return {
80+
albumId: i,
81+
id: i,
82+
title: 'accusamus beatae ad facilis cum similique qui sunt',
83+
url: 'https://via.placeholder.com/600/92c952',
84+
thumbnailUrl: 'https://via.placeholder.com/150/92c952',
85+
}
86+
})
87+
88+
const data = {
89+
testData,
90+
}
91+
92+
const runWithTimer = (data: any) => {
93+
const start = Date.now()
94+
const res = defaultSerializeQueryArgs({
95+
endpointDefinition,
96+
endpointName,
97+
queryArgs: data,
98+
})
99+
const end = Date.now()
100+
const duration = end - start
101+
return [res, duration] as const
102+
}
103+
104+
const [res1, time1] = runWithTimer(data)
105+
const [res2, time2] = runWithTimer(data)
106+
107+
expect(res1).toBe(res2)
108+
expect(time2).toBeLessThanOrEqual(time1)
109+
// Locally, stringifying 10K items takes 25-30ms.
110+
// Assuming the WeakMap cache hit, this _should_ be 0
111+
expect(time2).toBeLessThan(2)
112+
})

0 commit comments

Comments
 (0)