Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
45 changes: 44 additions & 1 deletion packages/react-meteor-data/useFind.tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,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 {

}
80 changes: 48 additions & 32 deletions packages/react-meteor-data/useFind.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,43 @@ type useFindActions<T> =
| { type: 'movedTo', fromIndex: number, toIndex: number }

const useFindReducer = <T>(data: T[], action: useFindActions<T>): T[] => {

// 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;
}
// -------

switch (action.type) {
case 'refresh':
return action.data
return mergeRefreshData(data, action.data);
case 'addedAt':
return [
...data.slice(0, action.atIndex),
Expand Down Expand Up @@ -82,50 +116,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 []
}

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)
initialData
);

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
}


// Perform the initial data fetch inside the effect.
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, _, atIndex) {
dispatch({ type: 'changedAt', document: newDocument, atIndex })
},
removedAt (oldDocument, atIndex) {
removedAt (_, atIndex) {
dispatch({ type: 'removedAt', atIndex })
},
movedTo (document, fromIndex, toIndex, before) {
movedTo (_, fromIndex, toIndex) {
dispatch({ type: 'movedTo', fromIndex, toIndex })
},
// @ts-ignore
Expand Down