Skip to content

Commit d110cb6

Browse files
committed
[X] Reuse doc refs when useFind's cursor deps change and add coverage for race/ordering cases (#1)
1 parent aabf046 commit d110cb6

2 files changed

Lines changed: 363 additions & 24 deletions

File tree

packages/react-meteor-data/useFind.tests.js

Lines changed: 297 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* global Meteor, Tinytest */
2-
import React, { memo, useState } from 'react'
2+
import React, { memo, useEffect, useLayoutEffect, useState } from 'react'
33
import ReactDOM from 'react-dom'
44
import { waitFor } from '@testing-library/react'
55
import { Mongo } from 'meteor/mongo'
@@ -150,6 +150,302 @@ if (Meteor.isClient) {
150150
completed();
151151
}
152152
);
153+
154+
Tinytest.addAsync(
155+
'useFind - reuses document references when deps recreate the cursor',
156+
async function (test, completed) {
157+
const container = document.createElement('div')
158+
159+
const TestDocs = new Mongo.Collection(null)
160+
for (let i = 0; i < 5; i++) {
161+
TestDocs.insert({ id: i })
162+
}
163+
164+
let renders = 0
165+
const MemoizedItem = memo(({ doc }) => {
166+
renders++
167+
return <li>{doc.id}</li>
168+
})
169+
170+
const Test = () => {
171+
const [rerendered, setRerendered] = useState(false)
172+
const docs = useFind(() => TestDocs.find({}, { sort: { id: 1 } }), [rerendered])
173+
174+
useEffect(() => {
175+
if (!rerendered) {
176+
setRerendered(true)
177+
}
178+
}, [rerendered])
179+
180+
return (
181+
<div>
182+
<div data-testid="rerender-flag">{String(rerendered)}</div>
183+
<ul>
184+
{docs.map(doc => (
185+
<MemoizedItem key={doc.id} doc={doc} />
186+
))}
187+
</ul>
188+
</div>
189+
)
190+
}
191+
192+
ReactDOM.render(<Test />, container)
193+
test.equal(renders, 5, 'Initial renders should occur once per document')
194+
195+
await waitFor(() => {
196+
const flag = container.querySelector('[data-testid="rerender-flag"]')
197+
if (!flag || flag.textContent !== 'true') {
198+
throw new Error('Component has not rerendered yet')
199+
}
200+
}, { container, timeout: 250 })
201+
202+
test.equal(
203+
renders,
204+
5,
205+
'Document references should remain stable when deps recreate the cursor'
206+
)
207+
208+
completed()
209+
}
210+
)
211+
212+
Tinytest.addAsync(
213+
'useFind - recreating cursor with modified documents re-renders memoized items',
214+
async function (test, completed) {
215+
const container = document.createElement('div')
216+
217+
const PrimaryDocs = new Mongo.Collection(null)
218+
const SecondaryDocs = new Mongo.Collection(null)
219+
220+
for (let i = 0; i < 4; i++) {
221+
PrimaryDocs.insert({ _id: `doc-${i}`, id: i, label: `primary-${i}` })
222+
SecondaryDocs.insert({ _id: `doc-${i}`, id: i, label: `secondary-${i}` })
223+
}
224+
225+
let renders = 0
226+
const MemoizedItem = memo(({ doc }) => {
227+
renders++
228+
return <li>{doc.label}</li>
229+
})
230+
231+
const Test = () => {
232+
const [useAlternate, setUseAlternate] = useState(false)
233+
const collection = useAlternate ? SecondaryDocs : PrimaryDocs
234+
const docs = useFind(() => collection.find({}, { sort: { id: 1 } }), [useAlternate])
235+
236+
useEffect(() => {
237+
if (!useAlternate) {
238+
setUseAlternate(true)
239+
}
240+
}, [useAlternate])
241+
242+
return (
243+
<ul>
244+
{docs.map(doc => (
245+
<MemoizedItem key={doc._id} doc={doc} />
246+
))}
247+
</ul>
248+
)
249+
}
250+
251+
ReactDOM.render(<Test />, container)
252+
test.equal(renders, 4, 'Initial renders should occur once per document')
253+
254+
await waitFor(() => {
255+
if (!container.textContent?.includes('secondary-0')) {
256+
throw new Error('Alternate collection has not rendered yet')
257+
}
258+
}, { container, timeout: 500 })
259+
260+
test.equal(
261+
renders,
262+
8,
263+
'Documents should rerender when deps recreate the cursor with modified data'
264+
)
265+
266+
completed()
267+
}
268+
)
269+
270+
Tinytest.addAsync(
271+
'useFind - recreating cursor with different sort order updates ordering',
272+
async function (test, completed) {
273+
const container = document.createElement('div')
274+
275+
const TestDocs = new Mongo.Collection(null)
276+
for (let i = 0; i < 4; i++) {
277+
TestDocs.insert({ id: i })
278+
}
279+
280+
const Test = () => {
281+
const [ascending, setAscending] = useState(true)
282+
const docs = useFind(
283+
() => TestDocs.find({}, { sort: { id: ascending ? 1 : -1 } }),
284+
[ascending]
285+
)
286+
287+
useEffect(() => {
288+
if (ascending) {
289+
setAscending(false)
290+
}
291+
}, [ascending])
292+
293+
return (
294+
<div>
295+
{docs.map(doc => (
296+
<div key={doc.id} data-testid="doc-id">{doc.id}</div>
297+
))}
298+
</div>
299+
)
300+
}
301+
302+
ReactDOM.render(<Test />, container)
303+
304+
const getIds = () => Array.from(
305+
container.querySelectorAll('[data-testid="doc-id"]')
306+
).map(node => node.textContent)
307+
308+
test.equal(getIds(), ['0', '1', '2', '3'], 'Initial render should be ascending')
309+
310+
await waitFor(() => {
311+
const ids = getIds()
312+
if (ids[0] !== '3') {
313+
throw new Error('Documents have not been reordered yet')
314+
}
315+
}, { container, timeout: 500 })
316+
317+
test.equal(
318+
getIds(),
319+
['3', '2', '1', '0'],
320+
'Documents should be rendered in descending order after deps change'
321+
)
322+
323+
completed()
324+
}
325+
)
326+
327+
Tinytest.addAsync(
328+
'useFind - recreating cursor with different projection re-renders memoized items',
329+
async function (test, completed) {
330+
const container = document.createElement('div')
331+
332+
const TestDocs = new Mongo.Collection(null)
333+
for (let i = 0; i < 3; i++) {
334+
TestDocs.insert({ _id: `doc-${i}`, id: i, label: `label-${i}`, detail: `detail-${i}` })
335+
}
336+
337+
let renders = 0
338+
const MemoizedItem = memo(({ doc }) => {
339+
renders++
340+
return <li>{doc.label}::{doc.detail || 'none'}</li>
341+
})
342+
343+
const Test = () => {
344+
const [includeDetail, setIncludeDetail] = useState(false)
345+
const docs = useFind(
346+
() => TestDocs.find(
347+
{},
348+
{
349+
sort: { id: 1 },
350+
transform: doc => {
351+
if (!includeDetail) {
352+
return { _id: doc._id, label: doc.label }
353+
}
354+
return { _id: doc._id, label: doc.label, detail: doc.detail }
355+
}
356+
}
357+
),
358+
[includeDetail]
359+
)
360+
361+
useEffect(() => {
362+
if (!includeDetail) {
363+
setIncludeDetail(true)
364+
}
365+
}, [includeDetail])
366+
367+
return (
368+
<ul>
369+
{docs.map(doc => (
370+
<MemoizedItem key={doc._id} doc={doc} />
371+
))}
372+
</ul>
373+
)
374+
}
375+
376+
ReactDOM.render(<Test />, container)
377+
test.equal(renders, 3, 'Initial renders should occur once per document')
378+
379+
await waitFor(() => {
380+
if (!container.textContent?.includes('detail-0')) {
381+
throw new Error('Projected detail fields have not rendered yet')
382+
}
383+
}, { container, timeout: 500 })
384+
385+
test.equal(
386+
renders,
387+
6,
388+
'Documents should rerender when cursor projection adds new fields'
389+
)
390+
391+
completed()
392+
}
393+
)
394+
395+
Tinytest.addAsync(
396+
'useFind - handles cursor recreation without duplicating documents',
397+
async function (test, completed) {
398+
const container = document.createElement('div')
399+
document.body.appendChild(container)
400+
401+
const TestDocs = new Mongo.Collection(null)
402+
TestDocs.insert({ id: 'a', label: 'a' })
403+
404+
const Test = () => {
405+
const [rerendered, setRerendered] = useState(false)
406+
const docs = useFind(() => TestDocs.find({}, { sort: { id: 1 } }), [rerendered])
407+
408+
useEffect(() => {
409+
setRerendered(true)
410+
}, [])
411+
412+
useLayoutEffect(() => {
413+
if (!rerendered || TestDocs.findOne({ id: 'b' })) {
414+
return
415+
}
416+
417+
TestDocs.insert({ id: 'b', label: 'b' })
418+
}, [rerendered])
419+
420+
return (
421+
<div>
422+
{docs && docs.map(doc => (
423+
<div key={doc.id} data-testid="doc-id">{doc.id}</div>
424+
))}
425+
</div>
426+
)
427+
}
428+
429+
ReactDOM.render(<Test />, container)
430+
431+
const getIds = () => Array.from(container.querySelectorAll('[data-testid=\"doc-id\"]')).map(node => node.textContent)
432+
433+
await waitFor(() => {
434+
const ids = getIds()
435+
if (ids.length !== 2) {
436+
throw new Error('Expected two documents')
437+
}
438+
if (ids[0] !== 'a' || ids[1] !== 'b') {
439+
throw new Error('Unexpected document order')
440+
}
441+
}, { container, timeout: 500 })
442+
443+
test.equal(getIds(), ['a', 'b'], 'Documents should not duplicate when deps change')
444+
445+
document.body.removeChild(container)
446+
completed()
447+
}
448+
)
153449
} else {
154450

155451
}

0 commit comments

Comments
 (0)