Skip to content

Commit 55752a9

Browse files
authored
Merge pull request #455 from welkinwong/master
fix(useTrackerSuspense): fix performance issues when multiple useTracker with suspense exist and rebuild test cases. Closes #454
2 parents 4b71138 + a83710a commit 55752a9

File tree

3 files changed

+411
-119
lines changed

3 files changed

+411
-119
lines changed
Lines changed: 343 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,343 @@
1+
/* global Meteor, Tinytest */
2+
import React, { Suspense } from 'react';
3+
import { renderToString } from 'react-dom/server';
4+
import { Mongo } from 'meteor/mongo';
5+
import { render } from '@testing-library/react';
6+
import { useTracker, cacheMap } from './useTracker';
7+
8+
const clearCache = async () => {
9+
await new Promise((resolve) => setTimeout(resolve, 100));
10+
cacheMap.clear();
11+
};
12+
13+
const setupTest = (data = { id: 0, updated: 0 }) => {
14+
const Coll = new Mongo.Collection(null);
15+
data && Coll.insertAsync(data);
16+
17+
return { Coll, simpleFetch: () => Coll.find().fetchAsync() };
18+
};
19+
20+
const TestSuspense = ({ children }) => {
21+
return <Suspense fallback={<div>Loading...</div>}>{children}</Suspense>;
22+
};
23+
24+
/**
25+
* Test for useTracker with Suspense
26+
*/
27+
Tinytest.addAsync(
28+
'suspense/useTracker - Data query validation',
29+
async (test) => {
30+
const { simpleFetch } = setupTest();
31+
32+
let returnValue;
33+
34+
const Test = () => {
35+
returnValue = useTracker('TestDocs', simpleFetch);
36+
37+
return null;
38+
};
39+
40+
// first return promise
41+
renderToString(
42+
<TestSuspense>
43+
<Test />
44+
</TestSuspense>
45+
);
46+
test.isUndefined(
47+
returnValue,
48+
'Return value should be undefined as find promise unresolved'
49+
);
50+
// wait promise
51+
await new Promise((resolve) => setTimeout(resolve, 100));
52+
// return data
53+
renderToString(
54+
<TestSuspense>
55+
<Test />
56+
</TestSuspense>
57+
);
58+
59+
test.equal(
60+
returnValue.length,
61+
1,
62+
'Return value should be an array with one document'
63+
);
64+
65+
await clearCache();
66+
}
67+
);
68+
69+
Tinytest.addAsync(
70+
'suspense/useTracker - Test proper cache invalidation',
71+
async function (test) {
72+
const { Coll, simpleFetch } = setupTest();
73+
74+
let returnValue;
75+
76+
const Test = () => {
77+
returnValue = useTracker('TestDocs', simpleFetch);
78+
return null;
79+
};
80+
81+
// first return promise
82+
renderToString(
83+
<TestSuspense>
84+
<Test />
85+
</TestSuspense>
86+
);
87+
// wait promise
88+
await new Promise((resolve) => setTimeout(resolve, 100));
89+
// return data
90+
renderToString(
91+
<TestSuspense>
92+
<Test />
93+
</TestSuspense>
94+
);
95+
96+
test.equal(
97+
returnValue[0].updated,
98+
0,
99+
'Return value should be an array with initial value as find promise resolved'
100+
);
101+
102+
Coll.updateAsync({ id: 0 }, { $inc: { updated: 1 } });
103+
await new Promise((resolve) => setTimeout(resolve, 100));
104+
105+
// second return promise
106+
renderToString(
107+
<TestSuspense>
108+
<Test />
109+
</TestSuspense>
110+
);
111+
112+
test.equal(
113+
returnValue[0].updated,
114+
0,
115+
'Return value should still not updated as second find promise unresolved'
116+
);
117+
118+
// wait promise
119+
await new Promise((resolve) => setTimeout(resolve, 100));
120+
// return data
121+
renderToString(
122+
<TestSuspense>
123+
<Test />
124+
</TestSuspense>
125+
);
126+
renderToString(
127+
<TestSuspense>
128+
<Test />
129+
</TestSuspense>
130+
);
131+
renderToString(
132+
<TestSuspense>
133+
<Test />
134+
</TestSuspense>
135+
);
136+
137+
test.equal(
138+
returnValue[0].updated,
139+
1,
140+
'Return value should be an array with one document with value updated'
141+
);
142+
143+
await clearCache();
144+
}
145+
);
146+
147+
Meteor.isClient &&
148+
Tinytest.addAsync(
149+
'suspense/useTracker - Test useTracker with skipUpdate',
150+
async function (test) {
151+
const { Coll, simpleFetch } = setupTest({ id: 0, updated: 0, other: 0 });
152+
153+
let returnValue;
154+
155+
const Test = () => {
156+
returnValue = useTracker('TestDocs', simpleFetch, (prev, next) => {
157+
// Skip update if the document has not changed
158+
return prev[0].updated === next[0].updated;
159+
});
160+
161+
return null;
162+
};
163+
164+
// first return promise
165+
renderToString(
166+
<TestSuspense>
167+
<Test />
168+
</TestSuspense>
169+
);
170+
// wait promise
171+
await new Promise((resolve) => setTimeout(resolve, 100));
172+
// return data
173+
renderToString(
174+
<TestSuspense>
175+
<Test />
176+
</TestSuspense>
177+
);
178+
179+
test.equal(
180+
returnValue[0].updated,
181+
0,
182+
'Return value should be an array with initial value as find promise resolved'
183+
);
184+
185+
Coll.updateAsync({ id: 0 }, { $inc: { other: 1 } });
186+
await new Promise((resolve) => setTimeout(resolve, 100));
187+
188+
// second return promise
189+
renderToString(
190+
<TestSuspense>
191+
<Test />
192+
</TestSuspense>
193+
);
194+
// wait promise
195+
await new Promise((resolve) => setTimeout(resolve, 100));
196+
// return data
197+
renderToString(
198+
<TestSuspense>
199+
<Test />
200+
</TestSuspense>
201+
);
202+
203+
test.equal(
204+
returnValue[0].other,
205+
0,
206+
'Return value should still not updated as skipUpdate returned true'
207+
);
208+
209+
await clearCache();
210+
}
211+
);
212+
213+
// https://github.com/meteor/react-packages/issues/454
214+
Meteor.isClient &&
215+
Tinytest.addAsync(
216+
'suspense/useTracker - Testing performance with multiple Trackers',
217+
async (test) => {
218+
const TestCollections = [];
219+
let returnDocs = new Map();
220+
221+
for (let i = 0; i < 100; i++) {
222+
const { Coll } = setupTest(null);
223+
224+
for (let i = 0; i < 100; i++) {
225+
Coll.insertAsync({ id: i });
226+
}
227+
228+
TestCollections.push(Coll);
229+
}
230+
231+
const Test = ({ collection, index }) => {
232+
const docsCount = useTracker(`TestDocs${index}`, () =>
233+
collection.find().fetchAsync()
234+
).length;
235+
236+
returnDocs.set(`TestDocs${index}`, docsCount);
237+
238+
return null;
239+
};
240+
const TestWrap = () => {
241+
return (
242+
<TestSuspense>
243+
{TestCollections.map((collection, index) => (
244+
<Test key={index} collection={collection} index={index} />
245+
))}
246+
</TestSuspense>
247+
);
248+
};
249+
250+
// first return promise
251+
renderToString(<TestWrap />);
252+
// wait promise
253+
await new Promise((resolve) => setTimeout(resolve, 100));
254+
// return data
255+
renderToString(<TestWrap />);
256+
257+
test.equal(returnDocs.size, 100, 'should return 100 collections');
258+
259+
const docsCount = Array.from(returnDocs.values()).reduce(
260+
(a, b) => a + b,
261+
0
262+
);
263+
264+
test.equal(docsCount, 10000, 'should return 10000 documents');
265+
266+
await clearCache();
267+
}
268+
);
269+
270+
Meteor.isServer &&
271+
Tinytest.addAsync(
272+
'suspense/useTracker - Test no memory leaks',
273+
async function (test) {
274+
const { simpleFetch } = setupTest();
275+
276+
let returnValue;
277+
278+
const Test = () => {
279+
returnValue = useTracker('TestDocs', simpleFetch);
280+
281+
return null;
282+
};
283+
284+
// first return promise
285+
renderToString(
286+
<TestSuspense>
287+
<Test />
288+
</TestSuspense>
289+
);
290+
// wait promise
291+
await new Promise((resolve) => setTimeout(resolve, 100));
292+
// return data
293+
renderToString(
294+
<TestSuspense>
295+
<Test />
296+
</TestSuspense>
297+
);
298+
// wait cleanup
299+
await new Promise((resolve) => setTimeout(resolve, 100));
300+
301+
test.equal(
302+
cacheMap.size,
303+
0,
304+
'Cache map should be empty as server cache should be cleared after render'
305+
);
306+
}
307+
);
308+
309+
Meteor.isClient &&
310+
Tinytest.addAsync(
311+
'suspense/useTracker - Test no memory leaks',
312+
async function (test) {
313+
const { simpleFetch } = setupTest({ id: 0, name: 'a' });
314+
315+
const Test = () => {
316+
const docs = useTracker('TestDocs', simpleFetch);
317+
318+
return <div>{docs[0]?.name}</div>;
319+
};
320+
321+
const { queryByText, findByText, unmount } = render(<Test />, {
322+
container: document.createElement('container'),
323+
wrapper: TestSuspense,
324+
});
325+
326+
test.isNotNull(
327+
queryByText('Loading...'),
328+
'Throw Promise as needed to trigger the fallback.'
329+
);
330+
331+
test.isTrue(await findByText('a'), 'Need to return data');
332+
333+
unmount();
334+
// wait cleanup
335+
await new Promise((resolve) => setTimeout(resolve, 100));
336+
337+
test.equal(
338+
cacheMap.size,
339+
0,
340+
'Cache map should be empty as component unmounted and cache cleared'
341+
);
342+
}
343+
);

0 commit comments

Comments
 (0)