diff --git a/packages/react-meteor-data/.meteorignore b/packages/react-meteor-data/.meteorignore
new file mode 100644
index 00000000..fc195154
--- /dev/null
+++ b/packages/react-meteor-data/.meteorignore
@@ -0,0 +1,3 @@
+node_modules
+package.json
+package-lock-json
diff --git a/packages/react-meteor-data/.versions b/packages/react-meteor-data/.versions
index 772780d6..0bbed27b 100644
--- a/packages/react-meteor-data/.versions
+++ b/packages/react-meteor-data/.versions
@@ -1,57 +1,56 @@
allow-deny@1.1.0
-babel-compiler@7.6.1
+babel-compiler@7.7.0
babel-runtime@1.5.0
base64@1.0.12
binary-heap@1.0.11
-blaze@2.3.4
+blaze@2.5.0
boilerplate-generator@1.7.1
-callback-hook@1.3.0
+callback-hook@1.4.0
check@1.3.1
ddp@1.4.0
-ddp-client@2.4.0
+ddp-client@2.5.0
ddp-common@1.4.0
-ddp-server@2.3.2
-deps@1.0.12
+ddp-server@2.5.0
diff-sequence@1.1.1
-dynamic-import@0.6.0
-ecmascript@0.15.1
-ecmascript-runtime@0.7.0
-ecmascript-runtime-client@0.11.0
-ecmascript-runtime-server@0.10.0
+dynamic-import@0.7.2
+ecmascript@0.16.0
+ecmascript-runtime@0.8.0
+ecmascript-runtime-client@0.12.1
+ecmascript-runtime-server@0.11.0
ejson@1.1.1
fetch@0.1.1
geojson-utils@1.0.10
-htmljs@1.0.11
-id-map@1.1.0
+htmljs@1.1.1
+id-map@1.1.1
inter-process-messaging@0.1.1
-local-test:react-meteor-data@2.3.3
-logging@1.2.0
-meteor@1.9.3
-minimongo@1.6.2
-modern-browsers@0.1.5
-modules@0.16.0
+local-test:react-meteor-data@2.4.0
+logging@1.3.1
+meteor@1.10.0
+minimongo@1.7.0
+modern-browsers@0.1.7
+modules@0.17.0
modules-runtime@0.12.0
-mongo@1.11.0
+mongo@1.13.0
mongo-decimal@0.1.2
mongo-dev-server@1.1.0
-mongo-id@1.0.7
-npm-mongo@3.9.0
-observe-sequence@1.0.16
+mongo-id@1.0.8
+npm-mongo@3.9.1
+observe-sequence@1.0.19
ordered-dict@1.1.0
-promise@0.11.2
+promise@0.12.0
random@1.2.0
-react-fast-refresh@0.1.0
-react-meteor-data@2.3.3
+react-fast-refresh@0.2.0
+react-meteor-data@2.4.0
reactive-dict@1.3.0
reactive-var@1.0.11
reload@1.3.1
retry@1.1.0
-routepolicy@1.1.0
-socket-stream-client@0.3.1
-test-helpers@1.2.0
-tinytest@1.1.0
+routepolicy@1.1.1
+socket-stream-client@0.4.0
+test-helpers@1.3.0
+tinytest@1.2.0
tracker@1.2.0
-typescript@4.2.2
+typescript@4.4.0
underscore@1.0.10
-webapp@1.10.1
+webapp@1.13.0
webapp-hashing@1.1.0
diff --git a/packages/react-meteor-data/CHANGELOG.md b/packages/react-meteor-data/CHANGELOG.md
index acb8baea..0ff81ad4 100644
--- a/packages/react-meteor-data/CHANGELOG.md
+++ b/packages/react-meteor-data/CHANGELOG.md
@@ -1,4 +1,7 @@
# CHANGELOG
+## v2.4.0, 2021-12-02
+* Added `useSubscribe` and `useFind` hooks
+
## v2.3.3, 2021-07-14
* Fixes a publication issue in v2.3.2
diff --git a/packages/react-meteor-data/README.md b/packages/react-meteor-data/README.md
index c2c6dd29..425269c1 100644
--- a/packages/react-meteor-data/README.md
+++ b/packages/react-meteor-data/README.md
@@ -231,6 +231,79 @@ export default withTracker({
})(Foo);
```
+#### `useSubscribe(subName, ...args)` A convenient wrapper for subscriptions
+
+`useSubscribe` is a convenient short hand for setting up a subscription. It is particularly useful when working with `useFind`, which should NOT be used for setting up subscriptions. At its core, it is a very simple wrapper around `useTracker` (with no deps) to create the subscription in a safe way, and allows you to avoid some of the cerimony around defining a factory and defining deps. Just pass the name of your subscription, and your arguments.
+
+`useSubscribe` returns an `isLoading` function. You can call `isLoading()` to react to changes in the subscription's loading state. The `isLoading` function will both return the loading state of the subscription, and set up a reactivity for the loading state change. If you don't call this function, no re-render will occur when the loading state changes.
+
+```jsx
+// Note: isLoading is a function!
+const isLoading = useSubscribe('posts', groupId);
+const posts = useFind(() => Posts.find({ groupId }), [groupId]);
+
+if (isLoading()) {
+ return
+} else {
+ return
+ {posts.map(post => - {post.title}
)}
+
+}
+```
+
+If you want to conditionally subscribe, you can set the `name` field (the first argument) to a falsy value to bypass the subscription.
+
+```jsx
+const needsData = false;
+const isLoading = useSubscribe(needsData ? "my-pub" : null);
+
+// When a subscription is not used, isLoading() will always return false
+```
+
+#### `useFind(cursorFactory, deps)` Accellerate your lists
+
+The `useFind` hook can substantially speed up the rendering (and rerendering) of lists coming from mongo queries (subscriptions). It does this by controlling document object references. By providing a highly tailored cursor management within the hook, using the `Cursor.observe` API, `useFind` carefully updates only the object references changed during a DDP update. This approach allows a tighter use of core React tools and philosophies to turbo charge your list renders. It is a very different approach from the more general purpose `useTracker`, and it requires a bit more set up. A notable difference is that you should NOT call `.fetch()`. `useFind` requires its factory to return a `Mongo.Cursor` object. You may also return `null`, if you want to conditionally set up the Cursor.
+
+Here is an example in code:
+
+```jsx
+import React, { memo } from 'react'
+import { useFind } from 'meteor/react-meteor-data'
+import TestDocs from '/imports/api/collections/TestDocs'
+
+// Memoize the list item
+const ListItem = memo(({doc}) => {
+ return (
+ {doc.id},{doc.updated}
+ )
+})
+
+const Test = () => {
+ const docs = useFind(() => TestDocs.find(), [])
+ return (
+
+ {docs.map(doc =>
+
+ )}
+
+ )
+}
+
+// Later on, update a single document - notice only that single component is updated in the DOM
+TestDocs.update({ id: 2 }, { $inc: { someProp: 1 } })
+```
+
+If you want to conditionally call the find method based on some props configuration or anything else, return `null` from the factory.
+
+```jsx
+const docs = useFind(() => {
+ if (props.skip) {
+ return null
+ }
+ return TestDocs.find()
+}, [])
+```
+
### Concurrent Mode, Suspense and Error Boundaries
There are some additional considerations to keep in mind when using Concurrent Mode, Suspense and Error Boundaries, as each of these can cause React to cancel and discard (toss) a render, including the result of the first run of your reactive function. One of the things React developers often stress is that we should not create "side-effects" directly in the render method or in functional components. There are a number of good reasons for this, including allowing the React runtime to cancel renders. Limiting the use of side-effects allows features such as concurrent mode, suspense and error boundaries to work deterministically, without leaking memory or creating rogue processes. Care should be taken to avoid side effects in your reactive function for these reasons. (Note: this caution does not apply to Meteor specific side-effects like subscriptions, since those will be automatically cleaned up when `useTracker`'s computation is disposed.)
diff --git a/packages/react-meteor-data/index.js b/packages/react-meteor-data/index.js
index b79056b0..9b9b708f 100644
--- a/packages/react-meteor-data/index.js
+++ b/packages/react-meteor-data/index.js
@@ -10,3 +10,5 @@ if (Meteor.isDevelopment) {
export { default as useTracker } from './useTracker';
export { default as withTracker } from './withTracker.tsx';
+export { useFind } from './useFind';
+export { useSubscribe } from './useSubscribe';
diff --git a/packages/react-meteor-data/package-lock.json b/packages/react-meteor-data/package-lock.json
index f33748ef..0ab59b3d 100644
--- a/packages/react-meteor-data/package-lock.json
+++ b/packages/react-meteor-data/package-lock.json
@@ -7,6 +7,7 @@
"version": "7.14.5",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz",
"integrity": "sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==",
+ "dev": true,
"requires": {
"@babel/highlight": "^7.14.5"
}
@@ -14,12 +15,14 @@
"@babel/helper-validator-identifier": {
"version": "7.14.5",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.5.tgz",
- "integrity": "sha512-5lsetuxCLilmVGyiLEfoHBRX8UCFD+1m2x3Rj97WrW3V7H3u4RWRXA4evMjImCsin2J2YT0QaVDGf+z8ondbAg=="
+ "integrity": "sha512-5lsetuxCLilmVGyiLEfoHBRX8UCFD+1m2x3Rj97WrW3V7H3u4RWRXA4evMjImCsin2J2YT0QaVDGf+z8ondbAg==",
+ "dev": true
},
"@babel/highlight": {
"version": "7.14.5",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz",
"integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==",
+ "dev": true,
"requires": {
"@babel/helper-validator-identifier": "^7.14.5",
"chalk": "^2.0.0",
@@ -30,6 +33,7 @@
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "dev": true,
"requires": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
@@ -42,6 +46,7 @@
"version": "7.14.6",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.6.tgz",
"integrity": "sha512-/PCB2uJ7oM44tz8YhC4Z/6PeOKXp4K588f+5M3clr1M4zbqztlo0XEfJ2LEzj/FgwfgGcIdl8n7YYjTCI0BYwg==",
+ "dev": true,
"requires": {
"regenerator-runtime": "^0.13.4"
}
@@ -50,6 +55,7 @@
"version": "7.14.7",
"resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.14.7.tgz",
"integrity": "sha512-Wvzcw4mBYbTagyBVZpAJWI06auSIj033T/yNE0Zn1xcup83MieCddZA7ls3kme17L4NOGBrQ09Q+nKB41RLWBA==",
+ "dev": true,
"requires": {
"core-js-pure": "^3.15.0",
"regenerator-runtime": "^0.13.4"
@@ -59,6 +65,7 @@
"version": "27.0.2",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-27.0.2.tgz",
"integrity": "sha512-XpjCtJ/99HB4PmyJ2vgmN7vT+JLP7RW1FBT9RgnMFS4Dt7cvIyBee8O3/j98aUZ34ZpenPZFqmaaObWSeL65dg==",
+ "dev": true,
"requires": {
"@types/istanbul-lib-coverage": "^2.0.0",
"@types/istanbul-reports": "^3.0.0",
@@ -71,6 +78,7 @@
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.0.0.tgz",
"integrity": "sha512-Ym375MTOpfszlagRnTMO+FOfTt6gRrWiDOWmEnWLu9OvwCPOWtK6i5pBHmZ07wUJiQ7wWz0t8+ZBK2wFo2tlew==",
+ "dev": true,
"requires": {
"@babel/code-frame": "^7.10.4",
"@babel/runtime": "^7.12.5",
@@ -86,28 +94,32 @@
"version": "12.0.0",
"resolved": "https://registry.npmjs.org/@testing-library/react/-/react-12.0.0.tgz",
"integrity": "sha512-sh3jhFgEshFyJ/0IxGltRhwZv2kFKfJ3fN1vTZ6hhMXzz9ZbbcTgmDYM4e+zJv+oiVKKEWZPyqPAh4MQBI65gA==",
+ "dev": true,
"requires": {
"@babel/runtime": "^7.12.5",
"@testing-library/dom": "^8.0.0"
}
},
"@types/aria-query": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.1.tgz",
- "integrity": "sha512-S6oPal772qJZHoRZLFc/XoZW2gFvwXusYUmXPXkgxJLuEk2vOt7jc4Yo6z/vtI0EBkbPBVrJJ0B+prLIKiWqHg=="
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.2.tgz",
+ "integrity": "sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig==",
+ "dev": true
},
"@types/bson": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/@types/bson/-/bson-4.0.4.tgz",
- "integrity": "sha512-awqorHvQS0DqxkHQ/FxcPX9E+H7Du51Qw/2F+5TBMSaE3G0hm+8D3eXJ6MAzFw75nE8V7xF0QvzUSdxIjJb/GA==",
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/@types/bson/-/bson-4.2.0.tgz",
+ "integrity": "sha512-ELCPqAdroMdcuxqwMgUpifQyRoTpyYCNr1V9xKyF40VsBobsj+BbWNRvwGchMgBPGqkw655ypkjj2MEF5ywVwg==",
+ "dev": true,
"requires": {
- "@types/node": "*"
+ "bson": "*"
}
},
"@types/connect": {
"version": "3.4.35",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
"integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==",
+ "dev": true,
"requires": {
"@types/node": "*"
}
@@ -115,12 +127,14 @@
"@types/istanbul-lib-coverage": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz",
- "integrity": "sha512-sz7iLqvVUg1gIedBOvlkxPlc8/uVzyS5OwGz1cKjXzkl3FpL3al0crU8YGU1WoHkxn0Wxbw5tyi6hvzJKNzFsw=="
+ "integrity": "sha512-sz7iLqvVUg1gIedBOvlkxPlc8/uVzyS5OwGz1cKjXzkl3FpL3al0crU8YGU1WoHkxn0Wxbw5tyi6hvzJKNzFsw==",
+ "dev": true
},
"@types/istanbul-lib-report": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz",
"integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==",
+ "dev": true,
"requires": {
"@types/istanbul-lib-coverage": "*"
}
@@ -129,17 +143,29 @@
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz",
"integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==",
+ "dev": true,
"requires": {
"@types/istanbul-lib-report": "*"
}
},
+ "@types/jquery": {
+ "version": "3.5.6",
+ "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.6.tgz",
+ "integrity": "sha512-SmgCQRzGPId4MZQKDj9Hqc6kSXFNWZFHpELkyK8AQhf8Zr6HKfCzFv9ZC1Fv3FyQttJZOlap3qYb12h61iZAIg==",
+ "dev": true,
+ "requires": {
+ "@types/sizzle": "*"
+ }
+ },
"@types/meteor": {
- "version": "1.4.73",
- "resolved": "https://registry.npmjs.org/@types/meteor/-/meteor-1.4.73.tgz",
- "integrity": "sha512-KnMtAF4A5Iwo+BHEU4XVTCZ8xLKyOlYFxA0mDHbjkCyLk2DiyCUjBZzbztuj0VyQNMqiVTvzO4LtiXozmRXPFg==",
+ "version": "1.4.78",
+ "resolved": "https://registry.npmjs.org/@types/meteor/-/meteor-1.4.78.tgz",
+ "integrity": "sha512-0OaRaRDZlM1pERRzaRubg63Iu/OUWd6Ygwd7oF47OtnhK9uYcZhkiZ2ElZVyqg2HZWlbU0oedVdeD80Ro8yiBg==",
+ "dev": true,
"requires": {
"@types/connect": "*",
- "@types/mongodb": "*",
+ "@types/jquery": "*",
+ "@types/mongodb": "^3.6.20",
"@types/react": "*",
"@types/underscore": "*"
}
@@ -148,6 +174,7 @@
"version": "3.6.20",
"resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-3.6.20.tgz",
"integrity": "sha512-WcdpPJCakFzcWWD9juKoZbRtQxKIMYF/JIAM4JrNHrMcnJL6/a2NWjXxW7fo9hxboxxkg+icff8d7+WIEvKgYQ==",
+ "dev": true,
"requires": {
"@types/bson": "*",
"@types/node": "*"
@@ -156,17 +183,20 @@
"@types/node": {
"version": "15.12.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-15.12.5.tgz",
- "integrity": "sha512-se3yX7UHv5Bscf8f1ERKvQOD6sTyycH3hdaoozvaLxgUiY5lIGEeH37AD0G0Qi9kPqihPn0HOfd2yaIEN9VwEg=="
+ "integrity": "sha512-se3yX7UHv5Bscf8f1ERKvQOD6sTyycH3hdaoozvaLxgUiY5lIGEeH37AD0G0Qi9kPqihPn0HOfd2yaIEN9VwEg==",
+ "dev": true
},
"@types/prop-types": {
"version": "15.7.4",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz",
- "integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ=="
+ "integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==",
+ "dev": true
},
"@types/react": {
- "version": "17.0.14",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.14.tgz",
- "integrity": "sha512-0WwKHUbWuQWOce61UexYuWTGuGY/8JvtUe/dtQ6lR4sZ3UiylHotJeWpf3ArP9+DSGUoLY3wbU59VyMrJps5VQ==",
+ "version": "17.0.26",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.26.tgz",
+ "integrity": "sha512-MXxuXrH2xOcv5cp/su4oz69dNQnSA90JjFw5HBd5wifw6Ihi94j7dRJm7qNsB30tnruXSCPc9qmlhGop4nh9Hw==",
+ "dev": true,
"requires": {
"@types/prop-types": "*",
"@types/scheduler": "*",
@@ -176,35 +206,47 @@
"@types/scheduler": {
"version": "0.16.2",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
- "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew=="
+ "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==",
+ "dev": true
+ },
+ "@types/sizzle": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz",
+ "integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==",
+ "dev": true
},
"@types/underscore": {
"version": "1.11.3",
"resolved": "https://registry.npmjs.org/@types/underscore/-/underscore-1.11.3.tgz",
- "integrity": "sha512-Fl1TX1dapfXyDqFg2ic9M+vlXRktcPJrc4PR7sRc7sdVrjavg/JHlbUXBt8qWWqhJrmSqg3RNAkAPRiOYw6Ahw=="
+ "integrity": "sha512-Fl1TX1dapfXyDqFg2ic9M+vlXRktcPJrc4PR7sRc7sdVrjavg/JHlbUXBt8qWWqhJrmSqg3RNAkAPRiOYw6Ahw==",
+ "dev": true
},
"@types/yargs": {
"version": "16.0.3",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.3.tgz",
"integrity": "sha512-YlFfTGS+zqCgXuXNV26rOIeETOkXnGQXP/pjjL9P0gO/EP9jTmc7pUBhx+jVEIxpq41RX33GQ7N3DzOSfZoglQ==",
+ "dev": true,
"requires": {
"@types/yargs-parser": "*"
}
},
"@types/yargs-parser": {
- "version": "20.2.0",
- "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-20.2.0.tgz",
- "integrity": "sha512-37RSHht+gzzgYeobbG+KWryeAW8J33Nhr69cjTqSYymXVZEN9NbRYWoYlRtDhHKPVT1FyNKwaTPC1NynKZpzRA=="
+ "version": "20.2.1",
+ "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-20.2.1.tgz",
+ "integrity": "sha512-7tFImggNeNBVMsn0vLrpn1H1uPrUBdnARPTpZoitY37ZrdJREzf7I16tMrlK3hen349gr1NYh8CmZQa7CTG6Aw==",
+ "dev": true
},
"ansi-regex": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
- "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg=="
+ "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
+ "dev": true
},
"ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dev": true,
"requires": {
"color-convert": "^1.9.0"
}
@@ -213,15 +255,42 @@
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-4.2.2.tgz",
"integrity": "sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA==",
+ "dev": true,
"requires": {
"@babel/runtime": "^7.10.2",
"@babel/runtime-corejs3": "^7.10.2"
}
},
+ "base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "dev": true
+ },
+ "bson": {
+ "version": "4.5.2",
+ "resolved": "https://registry.npmjs.org/bson/-/bson-4.5.2.tgz",
+ "integrity": "sha512-8CEMJpwc7qlQtrn2rney38jQSEeMar847lz0LyitwRmVknAW8iHXrzW4fTjHfyWm0E3sukyD/zppdH+QU1QefA==",
+ "dev": true,
+ "requires": {
+ "buffer": "^5.6.0"
+ }
+ },
+ "buffer": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+ "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+ "dev": true,
+ "requires": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.1.13"
+ }
+ },
"chalk": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz",
- "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==",
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
@@ -231,6 +300,7 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
"requires": {
"color-convert": "^2.0.1"
}
@@ -239,6 +309,7 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
"requires": {
"color-name": "~1.1.4"
}
@@ -246,17 +317,20 @@
"color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
- "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
},
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
- "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true
},
"supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
"requires": {
"has-flag": "^4.0.0"
}
@@ -267,6 +341,7 @@
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "dev": true,
"requires": {
"color-name": "1.1.3"
}
@@ -274,47 +349,62 @@
"color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
- "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
+ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
+ "dev": true
},
"core-js-pure": {
"version": "3.15.1",
"resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.15.1.tgz",
- "integrity": "sha512-OZuWHDlYcIda8sJLY4Ec6nWq2hRjlyCqCZ+jCflyleMkVt3tPedDVErvHslyS2nbO+SlBFMSBJYvtLMwxnrzjA=="
+ "integrity": "sha512-OZuWHDlYcIda8sJLY4Ec6nWq2hRjlyCqCZ+jCflyleMkVt3tPedDVErvHslyS2nbO+SlBFMSBJYvtLMwxnrzjA==",
+ "dev": true
},
"csstype": {
- "version": "3.0.8",
- "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.8.tgz",
- "integrity": "sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw=="
+ "version": "3.0.9",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.9.tgz",
+ "integrity": "sha512-rpw6JPxK6Rfg1zLOYCSwle2GFOOsnjmDYDaBwEcwoOg4qlsIVCN789VkBZDJAGi4T07gI4YSutR43t9Zz4Lzuw==",
+ "dev": true
},
"dom-accessibility-api": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.6.tgz",
- "integrity": "sha512-DplGLZd8L1lN64jlT27N9TVSESFR5STaEJvX+thCby7fuCHonfPpAlodYc3vuUYbDuDec5w8AMP7oCM5TWFsqw=="
+ "integrity": "sha512-DplGLZd8L1lN64jlT27N9TVSESFR5STaEJvX+thCby7fuCHonfPpAlodYc3vuUYbDuDec5w8AMP7oCM5TWFsqw==",
+ "dev": true
},
"escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
- "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
+ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
+ "dev": true
},
"fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
- "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true
},
"has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
- "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
+ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
+ "dev": true
+ },
+ "ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+ "dev": true
},
"js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
- "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true
},
"loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "dev": true,
"requires": {
"js-tokens": "^3.0.0 || ^4.0.0"
}
@@ -322,17 +412,20 @@
"lz-string": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz",
- "integrity": "sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY="
+ "integrity": "sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY=",
+ "dev": true
},
"object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
- "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
+ "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
+ "dev": true
},
"pretty-format": {
"version": "27.0.2",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.0.2.tgz",
"integrity": "sha512-mXKbbBPnYTG7Yra9qFBtqj+IXcsvxsvOBco3QHxtxTl+hHKq6QdzMZ+q0CtL4ORHZgwGImRr2XZUX2EWzORxig==",
+ "dev": true,
"requires": {
"@jest/types": "^27.0.2",
"ansi-regex": "^5.0.0",
@@ -343,7 +436,8 @@
"ansi-styles": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
- "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="
+ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
+ "dev": true
}
}
},
@@ -351,6 +445,7 @@
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz",
"integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==",
+ "dev": true,
"requires": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1"
@@ -360,6 +455,7 @@
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz",
"integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==",
+ "dev": true,
"requires": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
@@ -369,12 +465,14 @@
"react-is": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
- "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
+ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
+ "dev": true
},
"react-shallow-renderer": {
"version": "16.14.1",
"resolved": "https://registry.npmjs.org/react-shallow-renderer/-/react-shallow-renderer-16.14.1.tgz",
"integrity": "sha512-rkIMcQi01/+kxiTE9D3fdS959U1g7gs+/rborw++42m1O9FAQiNI/UNRZExVUoAOprn4umcXf+pFRou8i4zuBg==",
+ "dev": true,
"requires": {
"object-assign": "^4.1.1",
"react-is": "^16.12.0 || ^17.0.0"
@@ -384,6 +482,7 @@
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-17.0.2.tgz",
"integrity": "sha512-yaQ9cB89c17PUb0x6UfWRs7kQCorVdHlutU1boVPEsB8IDZH6n9tHxMacc3y0JoXOJUsZb/t/Mb8FUWMKaM7iQ==",
+ "dev": true,
"requires": {
"object-assign": "^4.1.1",
"react-is": "^17.0.2",
@@ -392,14 +491,16 @@
}
},
"regenerator-runtime": {
- "version": "0.13.7",
- "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
- "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew=="
+ "version": "0.13.9",
+ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz",
+ "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==",
+ "dev": true
},
"scheduler": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz",
"integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==",
+ "dev": true,
"requires": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1"
@@ -409,6 +510,7 @@
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
"requires": {
"has-flag": "^3.0.0"
}
diff --git a/packages/react-meteor-data/package.js b/packages/react-meteor-data/package.js
index c72e0902..e9342dc7 100644
--- a/packages/react-meteor-data/package.js
+++ b/packages/react-meteor-data/package.js
@@ -3,7 +3,7 @@
Package.describe({
name: 'react-meteor-data',
summary: 'React hook for reactively tracking Meteor data',
- version: '2.3.3',
+ version: '2.4.0',
documentation: 'README.md',
git: 'https://github.com/meteor/react-packages',
});
diff --git a/packages/react-meteor-data/package.json b/packages/react-meteor-data/package.json
index 801ee24e..fad69781 100644
--- a/packages/react-meteor-data/package.json
+++ b/packages/react-meteor-data/package.json
@@ -1,9 +1,9 @@
{
"name": "react-meteor-data",
- "dependencies": {
+ "devDependencies": {
"@testing-library/react": "^12.0.0",
- "@types/meteor": "^1.4.73",
- "@types/react": "^17.0.14",
+ "@types/meteor": "^1.4.76",
+ "@types/react": "^17.0.16",
"fast-deep-equal": "^3.1.3",
"react": "17.0.2",
"react-dom": "17.0.2",
diff --git a/packages/react-meteor-data/tests.js b/packages/react-meteor-data/tests.js
index e444261a..4a6aab3a 100644
--- a/packages/react-meteor-data/tests.js
+++ b/packages/react-meteor-data/tests.js
@@ -1,2 +1,3 @@
import './useTracker.tests.js';
import './withTracker.tests.js';
+import './useFind.tests.js';
diff --git a/packages/react-meteor-data/useFind.tests.js b/packages/react-meteor-data/useFind.tests.js
new file mode 100644
index 00000000..7c8114c3
--- /dev/null
+++ b/packages/react-meteor-data/useFind.tests.js
@@ -0,0 +1,114 @@
+/* global Meteor, Tinytest */
+import React, { memo, useState } from 'react'
+import ReactDOM from 'react-dom'
+import { waitFor } from '@testing-library/react'
+import { Mongo } from 'meteor/mongo'
+
+import { useFind } from './useFind'
+
+if (Meteor.isClient) {
+ Tinytest.addAsync('useFind - Verify reference stability between rerenders', async function (test, completed) {
+ const container = document.createElement("DIV")
+
+ const TestDocs = new Mongo.Collection(null)
+
+ TestDocs.insert({
+ id: 0,
+ updated: 0
+ })
+ TestDocs.insert({
+ id: 1,
+ updated: 0
+ })
+ TestDocs.insert({
+ id: 2,
+ updated: 0
+ })
+ TestDocs.insert({
+ id: 3,
+ updated: 0
+ })
+ TestDocs.insert({
+ id: 4,
+ updated: 0
+ })
+
+ let renders = 0
+ const MemoizedItem = memo(({doc}) => {
+ renders++
+ return (
+ {doc.id},{doc.updated}
+ )
+ })
+
+ const Test = () => {
+ const docs = useFind(() => TestDocs.find(), [])
+ return (
+
+ {docs.map(doc =>
+
+ )}
+
+ )
+ }
+
+ ReactDOM.render(, container)
+ test.equal(renders, 5, '5 items should have rendered, only 5, no more.')
+
+ await waitFor(() => {}, { container, timeout: 250 })
+
+ test.equal(renders, 10, '10 items should have rendered - the initial list is always tossed.')
+
+ await waitFor(() => {
+ TestDocs.update({ id: 2 }, { $inc: { updated: 1 } })
+ }, { container, timeout: 250 })
+
+ test.equal(renders, 11, '11 items should have rendered - only 1 of the items should have been matched by the reconciler after a single change.')
+
+ completed()
+ })
+
+ Tinytest.addAsync('useFind - null return is allowed', async function (test, completed) {
+ const container = document.createElement("DIV")
+
+ const TestDocs = new Mongo.Collection(null)
+
+ TestDocs.insert({
+ id: 0,
+ updated: 0
+ })
+
+ let setReturnNull, returnValue;
+
+ const Test = () => {
+ const [returnNull, _setReturnNull] = useState(true)
+ setReturnNull = _setReturnNull
+ const docs = useFind(() => returnNull ? null : TestDocs.find(), [returnNull])
+ returnValue = docs;
+ if (!docs) {
+ return null
+ } else {
+ return (
+
+ {docs.map(doc =>
+
+ )}
+
+ )
+ }
+ }
+
+ ReactDOM.render(, container)
+ test.isNull(returnValue, 'Return value should be null when the factory returns null')
+
+ setReturnNull(false)
+
+ await waitFor(() => {}, { container, timeout: 250 })
+ test.isNotNull(returnValue, 'Return value should be null when the factory returns null')
+
+ completed()
+ })
+
+} else {
+
+}
diff --git a/packages/react-meteor-data/useFind.ts b/packages/react-meteor-data/useFind.ts
new file mode 100644
index 00000000..2d0b156a
--- /dev/null
+++ b/packages/react-meteor-data/useFind.ts
@@ -0,0 +1,155 @@
+import { Meteor } from 'meteor/meteor'
+import { Mongo } from 'meteor/mongo'
+import { useReducer, useMemo, useEffect, Reducer, DependencyList, useRef } from 'react'
+import { Tracker } from 'meteor/tracker'
+
+type useFindActions =
+ | { type: 'refresh', data: T[] }
+ | { type: 'addedAt', document: T, atIndex: number }
+ | { type: 'changedAt', document: T, atIndex: number }
+ | { type: 'removedAt', atIndex: number }
+ | { type: 'movedTo', fromIndex: number, toIndex: number }
+
+const useFindReducer = (data: T[], action: useFindActions): T[] => {
+ switch (action.type) {
+ case 'refresh':
+ return action.data
+ case 'addedAt':
+ return [
+ ...data.slice(0, action.atIndex),
+ action.document,
+ ...data.slice(action.atIndex)
+ ]
+ case 'changedAt':
+ return [
+ ...data.slice(0, action.atIndex),
+ action.document,
+ ...data.slice(action.atIndex + 1)
+ ]
+ case 'removedAt':
+ return [
+ ...data.slice(0, action.atIndex),
+ ...data.slice(action.atIndex + 1)
+ ]
+ case 'movedTo':
+ const doc = data[action.fromIndex]
+ const copy = [
+ ...data.slice(0, action.fromIndex),
+ ...data.slice(action.fromIndex + 1)
+ ]
+ copy.splice(action.toIndex, 0, doc)
+ return copy
+ }
+}
+
+const checkCursor = (cursor: Mongo.Cursor | undefined | null) => {
+ if (cursor !== null && cursor !== undefined && !(cursor instanceof Mongo.Cursor)) {
+ console.warn(
+ 'Warning: useFind requires an instance of Mongo.Cursor. '
+ + 'Make sure you do NOT call .fetch() on your cursor.'
+ );
+ }
+}
+
+const useFindClient = (factory: () => (Mongo.Cursor | undefined | null), deps: DependencyList = []) => {
+ let [data, dispatch] = useReducer>>(
+ useFindReducer,
+ []
+ )
+
+ const { current: refs } = useRef<{ useReducerData: Boolean, data: T[] }>({ useReducerData: false, data: [] })
+
+ const cursor = useMemo(() => (
+ // To avoid creating side effects in render, opt out
+ // of Tracker integration altogether.
+ Tracker.nonreactive(() => {
+ refs.useReducerData = false
+ const c = factory()
+ if (Meteor.isDevelopment) {
+ checkCursor(c)
+ }
+ refs.data = (c instanceof Mongo.Cursor)
+ ? c.fetch()
+ : null
+ return c
+ })
+ ), deps)
+
+ useEffect(() => {
+ refs.useReducerData = true
+
+ if (!(cursor instanceof Mongo.Cursor)) {
+ return
+ }
+
+ // Refetch the data in case an update happened
+ // between first render and commit. Additionally,
+ // update in response to deps change.
+ const data = Tracker.nonreactive(() => cursor.fetch())
+
+ dispatch({
+ type: 'refresh',
+ data: data
+ })
+
+ const observer = cursor.observe({
+ addedAt (document, atIndex, before) {
+ dispatch({ type: 'addedAt', document, atIndex })
+ },
+ changedAt (newDocument, oldDocument, atIndex) {
+ dispatch({ type: 'changedAt', document: newDocument, atIndex })
+ },
+ removedAt (oldDocument, atIndex) {
+ dispatch({ type: 'removedAt', atIndex })
+ },
+ movedTo (document, fromIndex, toIndex, before) {
+ dispatch({ type: 'movedTo', fromIndex, toIndex })
+ },
+ // @ts-ignore
+ _suppress_initial: true
+ })
+
+ return () => {
+ observer.stop()
+ }
+ }, [cursor])
+
+ return refs.useReducerData ? data : refs.data
+}
+
+const useFindServer = (factory: () => Mongo.Cursor | undefined | null, deps: DependencyList) => (
+ Tracker.nonreactive(() => {
+ const cursor = factory()
+ if (Meteor.isDevelopment) checkCursor(cursor)
+ return (cursor instanceof Mongo.Cursor)
+ ? cursor.fetch()
+ : null
+ })
+)
+
+export const useFind = Meteor.isServer
+ ? useFindServer
+ : useFindClient
+
+function useFindDev (factory: () => (Mongo.Cursor | undefined | null), deps: DependencyList = []) {
+ function warn (expects: string, pos: string, arg: string, type: string) {
+ console.warn(
+ `Warning: useFind expected a ${expects} in it\'s ${pos} argument `
+ + `(${arg}), but got type of \`${type}\`.`
+ );
+ }
+
+ if (typeof factory !== 'function') {
+ warn("function", "1st", "reactiveFn", factory);
+ }
+
+ if (!deps || !Array.isArray(deps)) {
+ warn("array", "2nd", "deps", typeof deps);
+ }
+
+ return useFind(factory, deps);
+}
+
+export default Meteor.isDevelopment
+ ? useFindDev
+ : useFind;
diff --git a/packages/react-meteor-data/useSubscribe.ts b/packages/react-meteor-data/useSubscribe.ts
new file mode 100644
index 00000000..6fdae732
--- /dev/null
+++ b/packages/react-meteor-data/useSubscribe.ts
@@ -0,0 +1,28 @@
+import { Meteor } from 'meteor/meteor'
+import useTracker from './useTracker'
+
+const useSubscribeClient = (name?: string, ...args: any[]): () => boolean => {
+ let updateOnReady = false
+ let subscription: Meteor.SubscriptionHandle
+
+ const isReady = useTracker(() => {
+ if (!name) return true
+
+ subscription = Meteor.subscribe(name, ...args)
+
+ return subscription.ready()
+ }, () => (!updateOnReady))
+
+ return () => {
+ updateOnReady = true
+ return !isReady
+ }
+}
+
+const useSubscribeServer = (name?: string, ...args: any[]): () => boolean => (
+ () => false
+)
+
+export const useSubscribe = Meteor.isServer
+ ? useSubscribeServer
+ : useSubscribeClient
diff --git a/packages/react-meteor-data/useTracker.ts b/packages/react-meteor-data/useTracker.ts
index 33f816f5..e48d89aa 100644
--- a/packages/react-meteor-data/useTracker.ts
+++ b/packages/react-meteor-data/useTracker.ts
@@ -32,7 +32,7 @@ const fur = (x: number): number => x + 1;
const useForceUpdate = () => useReducer(fur, 0)[1];
export interface IReactiveFn {
- (c?: Tracker.Computation): T
+ (c?: Tracker.Computation): T
}
export interface ISkipUpdate {
@@ -120,6 +120,7 @@ const useTrackerWithDeps = (reactiveFn: IReactiveFn, deps: Dependenc
const { current: refs } = useRef<{
reactiveFn: IReactiveFn;
data?: T;
+ comp?: Tracker.Computation;
}>({ reactiveFn });
// keep reactiveFn ref fresh
@@ -133,11 +134,24 @@ const useTrackerWithDeps = (reactiveFn: IReactiveFn, deps: Dependenc
refs.data = refs.reactiveFn();
})
);
+ // In some cases, the useEffect hook will run before Meteor.defer, such as
+ // when React.lazy is used. This will allow it to be stopped earlier in
+ // useEffect if needed.
+ refs.comp = comp;
// To avoid creating side effects in render, stop the computation immediately
- Meteor.defer(() => { comp.stop() });
+ Meteor.defer(() => {
+ if (refs.comp) {
+ refs.comp.stop();
+ delete refs.comp;
+ }
+ });
}, deps);
useEffect(() => {
+ if (refs.comp) {
+ refs.comp.stop();
+ delete refs.comp;
+ }
const computation = Tracker.nonreactive(
() => Tracker.autorun((c) => {
const data: T = refs.reactiveFn(c);
diff --git a/packages/react-meteor-data/withTracker.tests.js b/packages/react-meteor-data/withTracker.tests.js
index faa3f156..5ffe1fba 100644
--- a/packages/react-meteor-data/withTracker.tests.js
+++ b/packages/react-meteor-data/withTracker.tests.js
@@ -36,7 +36,6 @@ if (Meteor.isClient) {
},
skipUpdate: skipUpdate,
})((props) => {
- console.log(props)
renders++;
return {JSON.stringify(props.value)};
});