Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions packages/react-meteor-data/useFind.tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,50 @@ if (Meteor.isClient) {
completed()
})

Tinytest.addAsync(
'useFind - Immediate update before effect registration (race condition test)',
async function (test, completed) {
const container = document.createElement('div');
document.body.appendChild(container);

const TestDocs = new Mongo.Collection(null);
// Insert a single document.
TestDocs.insert({ id: 1, val: 'initial' });

const Test = () => {
const docs = useFind(() => TestDocs.find(), []);
return (
<div data-testid="doc-value">
{docs && docs[0] && docs[0].val}
</div>
);
};

// Render the component.
ReactDOM.render(<Test />, container);

// Immediately update the document (this should occur
// after the synchronous fetch in the old code but before the effect attaches).
TestDocs.update({ id: 1 }, { $set: { val: 'updated' } });

// Wait until the rendered output reflects the update.
await waitFor(() => {
const node = container.querySelector('[data-testid="doc-value"]');
if (!node || !node.textContent.includes('updated')) {
throw new Error('Updated value not rendered yet');
}
}, { container, timeout: 500 });

test.ok(
container.innerHTML.includes('updated'),
'Document should display updated value; the old code would fail to capture this update.'
);

document.body.removeChild(container);
completed();
}
);

} else {

}
83 changes: 49 additions & 34 deletions packages/react-meteor-data/useFind.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Meteor } from 'meteor/meteor'
import { Mongo } from 'meteor/mongo'
import { useReducer, useMemo, useEffect, Reducer, DependencyList, useRef } from 'react'
import { useReducer, useMemo, useEffect, Reducer, DependencyList } from 'react'
import { Tracker } from 'meteor/tracker'

type useFindActions<T> =
Expand All @@ -10,10 +10,43 @@ type useFindActions<T> =
| { type: 'removedAt', atIndex: number }
| { type: 'movedTo', fromIndex: number, toIndex: number }

// Should I put this in a utils file?
const shallowEqual = (a: any, b: any) => {
if (a === b) return true;
if (!a || !b || typeof a !== 'object' || typeof b !== 'object') return false;
const keysA = Object.keys(a);
const keysB = Object.keys(b);
if (keysA.length !== keysB.length) return false;
for (const key of keysA) {
if (a[key] !== b[key]) return false;
}
return true;
};
const mergeRefreshData = <T>(oldData: T[], newData: T[]): T[] => {
if (oldData.length !== newData.length) return newData;

let changed = false;
const merged: T[] = new Array(newData.length);
const oldDocs = new Map(oldData.map(doc => [(doc as any).id, doc]));
// This verification is necessary for reference stability between rerenders
for (let i = 0; i < newData.length; i++) {
const newDoc = newData[i];
const oldDoc = oldDocs.get((newDoc as any).id);
if (oldDoc && shallowEqual(oldDoc, newDoc)) {
merged[i] = oldDoc;
} else {
merged[i] = newDoc;
changed = true;
}
}
return changed ? merged : oldData;
}
// -------

const useFindReducer = <T>(data: T[], action: useFindActions<T>): T[] => {
switch (action.type) {
case 'refresh':
return action.data
case 'refresh':
return mergeRefreshData(data, action.data);
case 'addedAt':
return [
...data.slice(0, action.atIndex),
Expand Down Expand Up @@ -82,50 +115,32 @@ const useFindClient = <T = any>(factory: () => (Mongo.Cursor<T> | undefined | nu
return cursor
}, deps)

const [data, dispatch] = useReducer<Reducer<T[], useFindActions<T>>, null>(
const initialData = cursor instanceof Mongo.Cursor ? fetchData(cursor) : [];
const [data, dispatch] = useReducer<Reducer<T[], useFindActions<T>>>(
useFindReducer,
null,
() => {
if (!(cursor instanceof Mongo.Cursor)) {
return []
}
initialData
);

return fetchData(cursor)
}
)

// Store information about mounting the component.
// It will be used to run code only if the component is updated.
const didMount = useRef(false)

useEffect(() => {
// Fetch intitial data if cursor was changed.
if (didMount.current) {
if (!(cursor instanceof Mongo.Cursor)) {
return
}

const data = fetchData(cursor)
dispatch({ type: 'refresh', data })
} else {
didMount.current = true
}

if (!(cursor instanceof Mongo.Cursor)) {
return
}


const newData = fetchData(cursor);
dispatch({ type: 'refresh', data: newData });

const observer = cursor.observe({
addedAt (document, atIndex, before) {
addedAt(document, atIndex) {
dispatch({ type: 'addedAt', document, atIndex })
},
changedAt (newDocument, oldDocument, atIndex) {
changedAt(newDocument, _oldDocument, atIndex) {
dispatch({ type: 'changedAt', document: newDocument, atIndex })
},
removedAt (oldDocument, atIndex) {
dispatch({ type: 'removedAt', atIndex })
removedAt(_oldDocument, atIndex) {
dispatch({ type: 'removedAt', atIndex });
},
movedTo (document, fromIndex, toIndex, before) {
movedTo(_document, fromIndex, toIndex) {
dispatch({ type: 'movedTo', fromIndex, toIndex })
},
// @ts-ignore
Expand Down