{
_.map(data.docs, (doc) =>
{doc._id})
@@ -482,18 +599,18 @@ if (Meteor.isClient) {
};
self.component = ReactDOM.render(
, self.div);
- test.equal(getInnerHtml(self.div), '
');
+ test.equal(getInnerHtml(self.div), '
', 'div should be empty');
var handle = self.handle;
- test.isFalse(handle.ready());
+ test.isFalse(handle.ready(), 'handle.ready() should be false');
waitForTracker(() => handle.ready(),
expect());
},
function (test, expect) {
var self = this;
- test.isTrue(self.handle.ready());
- test.equal(getInnerHtml(self.div), '
id1
');
+ test.isTrue(self.handle.ready(), 'self.handle.ready() should be true');
+ test.equal(getInnerHtml(self.div), '
id1
', 'div should contain id1');
self.someOtherVar.set('bar');
self.oldHandle1 = self.handle;
@@ -505,11 +622,11 @@ if (Meteor.isClient) {
var self = this;
var oldHandle = self.oldHandle1;
var newHandle = self.handle;
- test.notEqual(oldHandle, newHandle); // new handle
- test.equal(newHandle.subscriptionId, oldHandle.subscriptionId); // same sub
- test.isTrue(newHandle.ready()); // doesn't become unready
+ test.notEqual(oldHandle, newHandle, 'handles should be different instances'); // new handle
+ test.equal(newHandle.subscriptionId, oldHandle.subscriptionId, 'subscriptionId should be different'); // same sub
+ test.isTrue(newHandle.ready(), 'newHandle.ready() should be true'); // doesn't become unready
// no change to the content
- test.equal(getInnerHtml(self.div), '
id1
');
+ test.equal(getInnerHtml(self.div), '
id1
', 'div should contain id1');
// ok, now change the `num` argument to the subscription
self.num.set(2);
@@ -519,14 +636,14 @@ if (Meteor.isClient) {
function (test, expect) {
var self = this;
// data is still there
- test.equal(getInnerHtml(self.div), '
id1
');
+ test.equal(getInnerHtml(self.div), '
id1
', 'div shold contain id1');
// handle is no longer ready
var handle = self.handle;
- test.isFalse(handle.ready());
+ test.isFalse(handle.ready(), 'handle.ready() should be false');
// different sub ID
- test.isTrue(self.oldHandle2.subscriptionId);
- test.isTrue(handle.subscriptionId);
- test.notEqual(handle.subscriptionId, self.oldHandle2.subscriptionId);
+ test.isTrue(self.oldHandle2.subscriptionId, 'self.oldHandle2.subscriptionId should be truthy');
+ test.isTrue(handle.subscriptionId, 'handle.subscriptionId should be truthy');
+ test.notEqual(handle.subscriptionId, self.oldHandle2.subscriptionId, 'subscriptionId should match');
waitForTracker(() => handle.ready(),
expect());
@@ -548,11 +665,11 @@ if (Meteor.isClient) {
},
function (test, expect) {
var self = this;
- test.equal(self.data.v, 'baz');
- test.notEqual(self.oldHandle3, self.handle);
+ test.equal(self.data.v, 'baz', 'self.data.v should be "baz"');
+ test.notEqual(self.oldHandle3, self.handle, 'oldHandle3 shold match self.handle');
test.equal(self.oldHandle3.subscriptionId,
- self.handle.subscriptionId);
- test.isTrue(self.handle.ready());
+ self.handle.subscriptionId, 'same for subscriptionId');
+ test.isTrue(self.handle.ready(), 'self.handle.ready() should be true');
},
function (test, expect) {
ReactDOM.unmountComponentAtNode(this.div);
@@ -595,6 +712,12 @@ if (Meteor.isClient) {
// });
} else {
+ Meteor.publish("useTrackerLegacy-mixin-sub", function (num) {
+ Meteor.defer(() => { // because subs are blocking
+ this.added("useTrackerLegacy-mixin-coll", 'id'+num, {});
+ this.ready();
+ });
+ });
Meteor.publish("useTracker-mixin-sub", function (num) {
Meteor.defer(() => { // because subs are blocking
this.added("useTracker-mixin-coll", 'id'+num, {});
diff --git a/packages/react-meteor-data/useTracker.ts b/packages/react-meteor-data/useTracker.ts
index 76261a7e..06275439 100644
--- a/packages/react-meteor-data/useTracker.ts
+++ b/packages/react-meteor-data/useTracker.ts
@@ -1,10 +1,10 @@
declare var Package: any
import { Meteor } from 'meteor/meteor';
import { Tracker } from 'meteor/tracker';
-import { useReducer, useEffect, useRef, useMemo } from 'react';
+import { useReducer, useState, useEffect, useRef, useMemo, DependencyList } from 'react';
// Warns if data is a Mongo.Cursor or a POJO containing a Mongo.Cursor.
-function checkCursor(data: any): void {
+function checkCursor (data: any): void {
let shouldWarn = false;
if (Package.mongo && Package.mongo.Mongo && data && typeof data === 'object') {
if (data instanceof Package.mongo.Mongo.Cursor) {
@@ -29,56 +29,29 @@ function checkCursor(data: any): void {
// Used to create a forceUpdate from useReducer. Forces update by
// incrementing a number whenever the dispatch method is invoked.
const fur = (x: number): number => x + 1;
-const useForceUpdate = (): CallableFunction => {
- const [, forceUpdate] = useReducer(fur, 0);
- return forceUpdate;
-}
+const useForceUpdate = () => useReducer(fur, 0)[1];
type ReactiveFn = (c?: Tracker.Computation) => any;
-type ComputationHandler = (c: Tracker.Computation) => () => void | void;
type TrackerRefs = {
- reactiveFn: ReactiveFn;
- computationHandler?: ComputationHandler;
- deps?: Array
;
computation?: Tracker.Computation;
isMounted: boolean;
- disposeId?: ReturnType;
trackerData: any;
- computationCleanup?: () => void;
- trackerCount?: number
}
-// The follow functions were hoisted out of the closure to reduce allocations.
-// Since they no longer have access to the local vars, we pass them in and mutate here.
-const dispose = (refs: TrackerRefs): void => {
- if (refs.computationCleanup) {
- refs.computationCleanup();
- delete refs.computationCleanup;
- }
+const useTrackerNoDeps = (reactiveFn: ReactiveFn) => {
+ const { current: refs } = useRef({
+ isMounted: false,
+ trackerData: null
+ });
+ const forceUpdate = useForceUpdate();
+
+ // Without deps, always dispose and recreate the computation with every render.
if (refs.computation) {
refs.computation.stop();
- refs.computation = null;
+ // @ts-ignore This makes TS think ref.computation is "never" set
+ refs.computation;
}
-};
-const runReactiveFn = Meteor.isDevelopment
- ? (refs: TrackerRefs, c: Tracker.Computation): void => {
- const data = refs.reactiveFn(c);
- checkCursor(data);
- refs.trackerData = data;
- }
- : (refs: TrackerRefs, c: Tracker.Computation): void => {
- refs.trackerData = refs.reactiveFn(c);
- };
-
-const clear = (refs: TrackerRefs): void => {
- if (refs.disposeId) {
- clearTimeout(refs.disposeId);
- delete refs.disposeId;
- }
-};
-
-const track = (refs: TrackerRefs, forceUpdate: Function, trackedFn: Function): void => {
// Use Tracker.nonreactive in case we are inside a Tracker Computation.
// This can happen if someone calls `ReactDOM.render` inside a Computation.
// In that case, we want to opt out of the normal behavior of nested
@@ -86,79 +59,18 @@ const track = (refs: TrackerRefs, forceUpdate: Function, trackedFn: Function): v
// it stops the inner one.
Tracker.nonreactive(() => Tracker.autorun((c: Tracker.Computation) => {
refs.computation = c;
- trackedFn(c, refs, forceUpdate);
- }));
-};
-
-const doFirstRun = (refs: TrackerRefs, c: Tracker.Computation): void => {
- // If there is a computationHandler, pass it the computation, and store the
- // result, which may be a cleanup method.
- if (refs.computationHandler) {
- const cleanupHandler = refs.computationHandler(c);
- if (cleanupHandler) {
- if (Meteor.isDevelopment && typeof cleanupHandler !== 'function') {
- console.warn(
- 'Warning: Computation handler should return a function '
- + 'to be used for cleanup or return nothing.'
- );
- }
- refs.computationCleanup = cleanupHandler;
- }
- }
- // Always run the reactiveFn on firstRun
- runReactiveFn(refs, c);
-}
-
-const tracked = (c: Tracker.Computation, refs: TrackerRefs, forceUpdate: Function): void => {
- if (c.firstRun) {
- doFirstRun(refs, c);
- } else {
- if (refs.isMounted) {
- // Only run the reactiveFn if the component is mounted.
- runReactiveFn(refs, c);
- forceUpdate();
- } else {
- // If we got here, then a reactive update happened before the render was
- // committed - before useEffect has run. We don't want to run the reactiveFn
- // while we are not sure this render will be committed, so we'll dispose of the
- // computation, and set everything up to be restarted in useEffect if needed.
- // NOTE: If we don't run the user's reactiveFn when a computation updates, we'll
- // leave the computation in a non-reactive state - so we need to dispose here
- // and let useEffect recreate the computation later.
- dispose(refs);
- // Might as well clear the timeout!
- clear(refs);
- }
- }
-};
-
-interface useTrackerSignature {
- (reactiveFn: ReactiveFn, deps?: null | Array, computationHandler?: ComputationHandler): any
-}
-
-const useTrackerNoDeps: useTrackerSignature = (reactiveFn, deps = null, computationHandler) => {
- const { current: refs } = useRef({
- reactiveFn,
- isMounted: false,
- trackerData: null
- });
- const forceUpdate = useForceUpdate();
-
- refs.reactiveFn = reactiveFn;
- if (computationHandler) {
- refs.computationHandler = computationHandler;
- }
-
- // Without deps, always dispose and recreate the computation with every render.
- dispose(refs);
- track(refs, forceUpdate, (c: Tracker.Computation) => {
if (c.firstRun) {
- doFirstRun(refs, c);
+ // Always run the reactiveFn on firstRun
+ const data = reactiveFn(c);
+ if (Meteor.isDevelopment) {
+ checkCursor(data);
+ }
+ refs.trackerData = data;
} else {
// For any reactive change, forceUpdate and let the next render rebuild the computation.
forceUpdate();
}
- });
+ }));
// To avoid creating side effects in render with Tracker when not using deps
// create the computation, run the user's reactive function in a computation synchronously,
@@ -166,7 +78,10 @@ const useTrackerNoDeps: useTrackerSignature = (reactiveFn, deps = null, computat
if (!refs.isMounted) {
// We want to forceUpdate in useEffect to support StrictMode.
// See: https://github.com/meteor/react-packages/issues/278
- dispose(refs);
+ if (refs.computation) {
+ refs.computation.stop();
+ delete refs.computation;
+ }
}
useEffect(() => {
@@ -178,79 +93,45 @@ const useTrackerNoDeps: useTrackerSignature = (reactiveFn, deps = null, computat
forceUpdate();
// stop the computation on unmount
- return () => dispose(refs);
+ return () =>{
+ refs.computation?.stop();
+ }
}, []);
return refs.trackerData;
}
-const useTrackerWithDeps: useTrackerSignature = (reactiveFn, deps: Array, computationHandler) => {
- const { current: refs } = useRef({
- reactiveFn,
- isMounted: false,
- trackerData: null
- });
- const forceUpdate = useForceUpdate();
+const useTrackerClient = (reactiveFn: (c?: Tracker.Computation) => T, deps: DependencyList): T => {
+ let [data, setData] = useState();
- // Always have up to date deps and computations in all contexts
- refs.reactiveFn = reactiveFn;
- refs.deps = deps;
- if (computationHandler) {
- refs.computationHandler = computationHandler;
- }
-
- // We are abusing useMemo a little bit, using it for it's deps
- // compare, but not for it's memoization.
useMemo(() => {
- // stop the old one.
- dispose(refs);
-
- track(refs, forceUpdate, tracked)
-
- // Tracker creates side effect in render, which can be problematic in some cases, such as
- // Suspense or concurrent rendering or if an error is thrown and handled by an error boundary.
- // We still want synchronous rendering for a number of reasons (see readme). useTracker works
- // around memory/resource leaks by setting a time out to automatically clean everything up,
- // and watching a set of references to make sure everything is choreographed correctly.
- if (!refs.isMounted) {
- // Components yield to allow the DOM to update and the browser to paint before useEffect
- // is run. In concurrent mode this can take quite a long time. 1000ms should be enough
- // in most cases.
- refs.disposeId = setTimeout(() => {
- if (!refs.isMounted) {
- dispose(refs);
- }
- }, 1000);
+ // To jive with the lifecycle interplay between Tracker/Subscribe, run the
+ // reactive function in a computation, then stop it, to force flush cycle.
+ const comp = Tracker.nonreactive(
+ () => Tracker.autorun((c: Tracker.Computation) => {
+ if (c.firstRun) data = reactiveFn();
+ })
+ );
+ // To avoid creating side effects in render, stop the computation immediately
+ Meteor.defer(() => { comp.stop() });
+ if (Meteor.isDevelopment) {
+ checkCursor(data);
}
}, deps);
useEffect(() => {
- refs.isMounted = true;
-
- // Render is committed, clear the dispose timeout
- clear(refs);
-
- // If it took longer than 1000ms to get to useEffect, or a reactive update happened
- // before useEffect, restart the computation and forceUpdate.
- if (!refs.computation) {
- // This also runs runReactiveFn
- track(refs, forceUpdate, tracked);
- forceUpdate();
+ const computation = Tracker.autorun((c) => {
+ setData(reactiveFn(c));
+ });
+ return () => {
+ computation.stop();
}
+ }, deps);
- // stop the computation on unmount
- return () => dispose(refs);
- }, []);
-
- return refs.trackerData;
+ return data as T;
}
-const useTrackerClient: useTrackerSignature = (reactiveFn, deps = null, computationHandler) =>
- (deps === null || deps === undefined || !Array.isArray(deps))
- ? useTrackerNoDeps(reactiveFn, deps, computationHandler)
- : useTrackerWithDeps(reactiveFn, deps, computationHandler);
-
-const useTrackerServer: useTrackerSignature = (reactiveFn, deps = null, computationHandler) =>
+const useTrackerServer = (reactiveFn: () => T, deps: DependencyList): T =>
Tracker.nonreactive(reactiveFn);
// When rendering on the server, we don't want to use the Tracker.
@@ -259,28 +140,26 @@ const useTracker = Meteor.isServer
? useTrackerServer
: useTrackerClient;
-const useTrackerDev: useTrackerSignature = (reactiveFn, deps = null, computationHandler) => {
+const useTrackerDev = (reactiveFn: () => T, deps: DependencyList): T => {
if (typeof reactiveFn !== 'function') {
console.warn(
'Warning: useTracker expected a function in it\'s first argument '
+ `(reactiveFn), but got type of ${typeof reactiveFn}.`
);
}
- if (deps && !Array.isArray(deps)) {
+ if (!Array.isArray(deps)) {
console.warn(
'Warning: useTracker expected an array in it\'s second argument '
+ `(dependency), but got type of ${typeof deps}.`
);
}
- if (computationHandler && typeof computationHandler !== 'function') {
- console.warn(
- 'Warning: useTracker expected a function in it\'s third argument'
- + `(computationHandler), but got type of ${typeof computationHandler}.`
- );
- }
- return useTracker(reactiveFn, deps, computationHandler);
+ return useTracker(reactiveFn, deps);
}
export default Meteor.isDevelopment
? useTrackerDev
: useTracker;
+
+export const useTrackerLegacy = Meteor.isServer
+? useTrackerServer
+: useTrackerNoDeps;
diff --git a/packages/react-meteor-data/withTracker.tests.js b/packages/react-meteor-data/withTracker.tests.js
index 31f666bd..5f7d44a9 100644
--- a/packages/react-meteor-data/withTracker.tests.js
+++ b/packages/react-meteor-data/withTracker.tests.js
@@ -141,66 +141,6 @@ if (Meteor.isClient) {
completed();
});
- Tinytest.addAsync('withTracker - track based on props and state (with deps)', async function (test, completed) {
- var container = document.createElement("DIV");
-
- var xs = [new ReactiveVar('aaa'),
- new ReactiveVar('bbb'),
- new ReactiveVar('ccc')];
-
- let setState;
- var Foo = (props) => {
- const [state, _setState] = useState({ m: 0 });
- setState = _setState;
- const Component = withTracker(() => {
- return {
- x: xs[state.m + props.n].get()
- };
- })((props) => {
- return {props.x};
- });
- return
- };
-
- ReactDOM.render(, container);
-
- test.equal(getInnerHtml(container), 'aaa');
-
- xs[0].set('AAA');
- await waitFor(() => {
- Tracker.flush({_throwFirstError: true});
- }, { container, timeout: 250 });
- test.equal(getInnerHtml(container), 'AAA');
-
- xs[1].set('BBB');
- setState({m: 1});
- await waitFor(() => {
- Tracker.flush({_throwFirstError: true});
- }, { container, timeout: 250 });
- test.equal(getInnerHtml(container), 'BBB');
-
- setState({m: 2});
- await waitFor(() => {
- Tracker.flush({_throwFirstError: true});
- }, { container, timeout: 250 });
- test.equal(getInnerHtml(container), 'ccc');
- xs[2].set('CCC');
- await waitFor(() => {
- Tracker.flush({_throwFirstError: true});
- }, { container, timeout: 250 });
- test.equal(getInnerHtml(container), 'CCC');
-
- ReactDOM.unmountComponentAtNode(container);
-
- ReactDOM.render(, container);
- setState({m: 0});
- test.equal(getInnerHtml(container), 'AAA');
-
- ReactDOM.unmountComponentAtNode(container);
-
- completed();
- });
-
function waitForTracker(func, callback) {
Tracker.autorun(function (c) {
if (func()) {
diff --git a/packages/react-meteor-data/withTracker.tsx b/packages/react-meteor-data/withTracker.tsx
index 94862f1c..6cff054a 100644
--- a/packages/react-meteor-data/withTracker.tsx
+++ b/packages/react-meteor-data/withTracker.tsx
@@ -1,5 +1,5 @@
import React, { forwardRef, memo } from 'react';
-import useTracker from './useTracker';
+import { useTrackerLegacy } from './useTracker';
type ReactiveFn = (props: object) => any;
type ReactiveOptions = {
@@ -14,7 +14,7 @@ export default function withTracker(options: ReactiveFn | ReactiveOptions) {
: options.getMeteorData;
const WithTracker = forwardRef((props, ref) => {
- const data = useTracker(() => getMeteorData(props) || {});
+ const data = useTrackerLegacy(() => getMeteorData(props) || {});
return (
);
diff --git a/packages/react-meteor-state/README.md b/packages/react-meteor-state/README.md
new file mode 100644
index 00000000..de03b5c9
--- /dev/null
+++ b/packages/react-meteor-state/README.md
@@ -0,0 +1,4 @@
+meteor/react-meteor-state
+=========================
+
+A state hook, with an API similar to React's useState, which provides data persistence between page reloads using hte Meteor reload package.
diff --git a/packages/react-meteor-state/package-lock.json b/packages/react-meteor-state/package-lock.json
new file mode 100644
index 00000000..51ecb4b2
--- /dev/null
+++ b/packages/react-meteor-state/package-lock.json
@@ -0,0 +1,430 @@
+{
+ "name": "react-meteor-state",
+ "requires": true,
+ "lockfileVersion": 1,
+ "dependencies": {
+ "@babel/code-frame": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz",
+ "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==",
+ "requires": {
+ "@babel/highlight": "^7.10.4"
+ }
+ },
+ "@babel/helper-validator-identifier": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz",
+ "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw=="
+ },
+ "@babel/highlight": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz",
+ "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==",
+ "requires": {
+ "@babel/helper-validator-identifier": "^7.10.4",
+ "chalk": "^2.0.0",
+ "js-tokens": "^4.0.0"
+ },
+ "dependencies": {
+ "chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "requires": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ }
+ }
+ }
+ },
+ "@babel/runtime": {
+ "version": "7.12.1",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.1.tgz",
+ "integrity": "sha512-J5AIf3vPj3UwXaAzb5j1xM4WAQDX3EMgemF8rjCP3SoW09LfRKAXQKt6CoVYl230P6iWdRcBbnLDDdnqWxZSCA==",
+ "requires": {
+ "regenerator-runtime": "^0.13.4"
+ }
+ },
+ "@babel/runtime-corejs3": {
+ "version": "7.12.1",
+ "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.12.1.tgz",
+ "integrity": "sha512-umhPIcMrlBZ2aTWlWjUseW9LjQKxi1dpFlQS8DzsxB//5K+u6GLTC/JliPKHsd5kJVPIU6X/Hy0YvWOYPcMxBw==",
+ "requires": {
+ "core-js-pure": "^3.0.0",
+ "regenerator-runtime": "^0.13.4"
+ }
+ },
+ "@jest/types": {
+ "version": "26.5.2",
+ "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.5.2.tgz",
+ "integrity": "sha512-QDs5d0gYiyetI8q+2xWdkixVQMklReZr4ltw7GFDtb4fuJIBCE6mzj2LnitGqCuAlLap6wPyb8fpoHgwZz5fdg==",
+ "requires": {
+ "@types/istanbul-lib-coverage": "^2.0.0",
+ "@types/istanbul-reports": "^3.0.0",
+ "@types/node": "*",
+ "@types/yargs": "^15.0.0",
+ "chalk": "^4.0.0"
+ }
+ },
+ "@testing-library/dom": {
+ "version": "7.26.0",
+ "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-7.26.0.tgz",
+ "integrity": "sha512-fyKFrBbS1IigaE3FV21LyeC7kSGF84lqTlSYdKmGaHuK2eYQ/bXVPM5vAa2wx/AU1iPD6oQHsxy2QQ17q9AMCg==",
+ "requires": {
+ "@babel/code-frame": "^7.10.4",
+ "@babel/runtime": "^7.10.3",
+ "@types/aria-query": "^4.2.0",
+ "aria-query": "^4.2.2",
+ "chalk": "^4.1.0",
+ "dom-accessibility-api": "^0.5.1",
+ "lz-string": "^1.4.4",
+ "pretty-format": "^26.4.2"
+ }
+ },
+ "@testing-library/react": {
+ "version": "10.4.9",
+ "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-10.4.9.tgz",
+ "integrity": "sha512-pHZKkqUy0tmiD81afs8xfiuseXfU/N7rAX3iKjeZYje86t9VaB0LrxYVa+OOsvkrveX5jCK3IjajVn2MbePvqA==",
+ "requires": {
+ "@babel/runtime": "^7.10.3",
+ "@testing-library/dom": "^7.22.3"
+ }
+ },
+ "@types/aria-query": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.0.tgz",
+ "integrity": "sha512-iIgQNzCm0v7QMhhe4Jjn9uRh+I6GoPmt03CbEtwx3ao8/EfoQcmgtqH4vQ5Db/lxiIGaWDv6nwvunuh0RyX0+A=="
+ },
+ "@types/bson": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/bson/-/bson-4.0.2.tgz",
+ "integrity": "sha512-+uWmsejEHfmSjyyM/LkrP0orfE2m5Mx9Xel4tXNeqi1ldK5XMQcDsFkBmLDtuyKUbxj2jGDo0H240fbCRJZo7Q==",
+ "requires": {
+ "@types/node": "*"
+ }
+ },
+ "@types/connect": {
+ "version": "3.4.33",
+ "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.33.tgz",
+ "integrity": "sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A==",
+ "requires": {
+ "@types/node": "*"
+ }
+ },
+ "@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=="
+ },
+ "@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==",
+ "requires": {
+ "@types/istanbul-lib-coverage": "*"
+ }
+ },
+ "@types/istanbul-reports": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz",
+ "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==",
+ "requires": {
+ "@types/istanbul-lib-report": "*"
+ }
+ },
+ "@types/meteor": {
+ "version": "1.4.57",
+ "resolved": "https://registry.npmjs.org/@types/meteor/-/meteor-1.4.57.tgz",
+ "integrity": "sha512-Kng//SdSaDXnwXmUyUb45e/as3bneaoanTHDqEm1R9gcijqiXz0Gofw0+RaE0U3dB5ej5w2JnUuLR90P+JAhcA==",
+ "requires": {
+ "@types/connect": "*",
+ "@types/mongodb": "*",
+ "@types/react": "*",
+ "@types/underscore": "*"
+ }
+ },
+ "@types/mongodb": {
+ "version": "3.5.28",
+ "resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-3.5.28.tgz",
+ "integrity": "sha512-wSbda4QQ3QFsWUHJzC5TSYImtRDtPhPb+lW7ICMtWvvtQMQRXbVTz/Z1P5A3XujK5bZIAV7vk5CXQG6Adz7+Cw==",
+ "requires": {
+ "@types/bson": "*",
+ "@types/node": "*"
+ }
+ },
+ "@types/node": {
+ "version": "14.11.10",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-14.11.10.tgz",
+ "integrity": "sha512-yV1nWZPlMFpoXyoknm4S56y2nlTAuFYaJuQtYRAOU7xA/FJ9RY0Xm7QOkaYMMmr8ESdHIuUb6oQgR/0+2NqlyA=="
+ },
+ "@types/prop-types": {
+ "version": "15.7.3",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz",
+ "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw=="
+ },
+ "@types/react": {
+ "version": "16.9.53",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.53.tgz",
+ "integrity": "sha512-4nW60Sd4L7+WMXH1D6jCdVftuW7j4Za6zdp6tJ33Rqv0nk1ZAmQKML9ZLD4H0dehA3FZxXR/GM8gXplf82oNGw==",
+ "requires": {
+ "@types/prop-types": "*",
+ "csstype": "^3.0.2"
+ }
+ },
+ "@types/underscore": {
+ "version": "1.10.24",
+ "resolved": "https://registry.npmjs.org/@types/underscore/-/underscore-1.10.24.tgz",
+ "integrity": "sha512-T3NQD8hXNW2sRsSbLNjF/aBo18MyJlbw0lSpQHB/eZZtScPdexN4HSa8cByYwTw9Wy7KuOFr81mlDQcQQaZ79w=="
+ },
+ "@types/yargs": {
+ "version": "15.0.9",
+ "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.9.tgz",
+ "integrity": "sha512-HmU8SeIRhZCWcnRskCs36Q1Q00KBV6Cqh/ora8WN1+22dY07AZdn6Gel8QZ3t26XYPImtcL8WV/eqjhVmMEw4g==",
+ "requires": {
+ "@types/yargs-parser": "*"
+ }
+ },
+ "@types/yargs-parser": {
+ "version": "15.0.0",
+ "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-15.0.0.tgz",
+ "integrity": "sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw=="
+ },
+ "ansi-regex": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
+ "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg=="
+ },
+ "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==",
+ "requires": {
+ "color-convert": "^1.9.0"
+ }
+ },
+ "aria-query": {
+ "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==",
+ "requires": {
+ "@babel/runtime": "^7.10.2",
+ "@babel/runtime-corejs3": "^7.10.2"
+ }
+ },
+ "chalk": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
+ "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
+ "requires": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "dependencies": {
+ "ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "requires": {
+ "color-convert": "^2.0.1"
+ }
+ },
+ "color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "requires": {
+ "color-name": "~1.1.4"
+ }
+ },
+ "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=="
+ },
+ "has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
+ },
+ "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==",
+ "requires": {
+ "has-flag": "^4.0.0"
+ }
+ }
+ }
+ },
+ "color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "requires": {
+ "color-name": "1.1.3"
+ }
+ },
+ "color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
+ },
+ "core-js-pure": {
+ "version": "3.6.5",
+ "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.6.5.tgz",
+ "integrity": "sha512-lacdXOimsiD0QyNf9BC/mxivNJ/ybBGJXQFKzRekp1WTHoVUWsUHEn+2T8GJAzzIhyOuXA+gOxCVN3l+5PLPUA=="
+ },
+ "csstype": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.3.tgz",
+ "integrity": "sha512-jPl+wbWPOWJ7SXsWyqGRk3lGecbar0Cb0OvZF/r/ZU011R4YqiRehgkQ9p4eQfo9DSDLqLL3wHwfxeJiuIsNag=="
+ },
+ "dom-accessibility-api": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.4.tgz",
+ "integrity": "sha512-TvrjBckDy2c6v6RLxPv5QXOnU+SmF9nBII5621Ve5fu6Z/BDrENurBEvlC1f44lKEUVqOpK4w9E5Idc5/EgkLQ=="
+ },
+ "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="
+ },
+ "has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
+ },
+ "js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
+ },
+ "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==",
+ "requires": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ }
+ },
+ "lz-string": {
+ "version": "1.4.4",
+ "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz",
+ "integrity": "sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY="
+ },
+ "object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
+ },
+ "pretty-format": {
+ "version": "26.5.2",
+ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.5.2.tgz",
+ "integrity": "sha512-VizyV669eqESlkOikKJI8Ryxl/kPpbdLwNdPs2GrbQs18MpySB5S0Yo0N7zkg2xTRiFq4CFw8ct5Vg4a0xP0og==",
+ "requires": {
+ "@jest/types": "^26.5.2",
+ "ansi-regex": "^5.0.0",
+ "ansi-styles": "^4.0.0",
+ "react-is": "^16.12.0"
+ },
+ "dependencies": {
+ "ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "requires": {
+ "color-convert": "^2.0.1"
+ }
+ },
+ "color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "requires": {
+ "color-name": "~1.1.4"
+ }
+ },
+ "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=="
+ }
+ }
+ },
+ "prop-types": {
+ "version": "15.7.2",
+ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
+ "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==",
+ "requires": {
+ "loose-envify": "^1.4.0",
+ "object-assign": "^4.1.1",
+ "react-is": "^16.8.1"
+ }
+ },
+ "react": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react/-/react-16.13.1.tgz",
+ "integrity": "sha512-YMZQQq32xHLX0bz5Mnibv1/LHb3Sqzngu7xstSM+vrkE5Kzr9xE0yMByK5kMoTK30YVJE61WfbxIFFvfeDKT1w==",
+ "requires": {
+ "loose-envify": "^1.1.0",
+ "object-assign": "^4.1.1",
+ "prop-types": "^15.6.2"
+ }
+ },
+ "react-dom": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.13.1.tgz",
+ "integrity": "sha512-81PIMmVLnCNLO/fFOQxdQkvEq/+Hfpv24XNJfpyZhTRfO0QcmQIF/PgCa1zCOj2w1hrn12MFLyaJ/G0+Mxtfag==",
+ "requires": {
+ "loose-envify": "^1.1.0",
+ "object-assign": "^4.1.1",
+ "prop-types": "^15.6.2",
+ "scheduler": "^0.19.1"
+ }
+ },
+ "react-is": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
+ },
+ "react-test-renderer": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.13.1.tgz",
+ "integrity": "sha512-Sn2VRyOK2YJJldOqoh8Tn/lWQ+ZiKhyZTPtaO0Q6yNj+QDbmRkVFap6pZPy3YQk8DScRDfyqm/KxKYP9gCMRiQ==",
+ "requires": {
+ "object-assign": "^4.1.1",
+ "prop-types": "^15.6.2",
+ "react-is": "^16.8.6",
+ "scheduler": "^0.19.1"
+ }
+ },
+ "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=="
+ },
+ "scheduler": {
+ "version": "0.19.1",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz",
+ "integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==",
+ "requires": {
+ "loose-envify": "^1.1.0",
+ "object-assign": "^4.1.1"
+ }
+ },
+ "supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "requires": {
+ "has-flag": "^3.0.0"
+ }
+ },
+ "typescript": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.0.3.tgz",
+ "integrity": "sha512-tEu6DGxGgRJPb/mVPIZ48e69xCn2yRmCgYmDugAVwmJ6o+0u1RI18eO7E7WBTLYLaEVVOhwQmcdhQHweux/WPg=="
+ }
+ }
+}
diff --git a/packages/react-meteor-state/package.js b/packages/react-meteor-state/package.js
new file mode 100644
index 00000000..df1b9bff
--- /dev/null
+++ b/packages/react-meteor-state/package.js
@@ -0,0 +1,17 @@
+/* global Package */
+
+Package.describe({
+ name: 'react-meteor-state',
+ summary: 'React hook for reactively tracking Meteor data',
+ version: '0.9.0',
+ documentation: 'README.md',
+ git: 'https://github.com/meteor/react-packages',
+});
+
+Package.onUse(function (api) {
+ api.versionsFrom('1.10');
+ api.use('tracker');
+ api.use('typescript');
+
+ api.mainModule('use-meteor-state.ts', ['client', 'server'], { lazy: true });
+});
diff --git a/packages/react-meteor-state/package.json b/packages/react-meteor-state/package.json
new file mode 100644
index 00000000..14b0269d
--- /dev/null
+++ b/packages/react-meteor-state/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "meteor-react-meteor-state",
+ "scripts": {
+ "make-types": "npx typescript *.ts --declaration --emitDeclarationOnly --outDir types"
+ },
+ "dependencies": {
+ "@testing-library/react": "^10.0.2",
+ "@types/meteor": "^1.4.42",
+ "@types/react": "^16.9.34",
+ "react": "16.13.1",
+ "react-dom": "16.13.1",
+ "react-test-renderer": "16.13.1",
+ "typescript": "^4.0.3"
+ }
+}
diff --git a/packages/react-meteor-state/types/use-meteor-state.d.ts b/packages/react-meteor-state/types/use-meteor-state.d.ts
new file mode 100644
index 00000000..05ea1912
--- /dev/null
+++ b/packages/react-meteor-state/types/use-meteor-state.d.ts
@@ -0,0 +1,3 @@
+import { SetStateAction, Dispatch } from 'react';
+declare const _default: (initialValue: S | (() => S), name: string) => [S, Dispatch>];
+export default _default;
diff --git a/packages/react-meteor-state/use-meteor-state.ts b/packages/react-meteor-state/use-meteor-state.ts
new file mode 100644
index 00000000..fcda7a47
--- /dev/null
+++ b/packages/react-meteor-state/use-meteor-state.ts
@@ -0,0 +1,47 @@
+import { Meteor } from 'meteor/meteor'
+import { Reload } from 'meteor/reload'
+import { useState, useEffect, SetStateAction, Dispatch } from 'react'
+
+interface IHash {
+ [key: string] : any
+}
+
+if (Meteor.isClient) {
+ var toMigrate: IHash = {}
+ var migrated: IHash = Reload._migrationData('use-meteor-state') || {}
+
+ Reload._onMigrate('use-meteor-state', () => [true, toMigrate])
+}
+
+const useMeteorState = (initialValue: S | (() => S), name: string): [S, Dispatch>] => {
+ // When running in concurrent mode, this may run multiple times ...
+ if (migrated[name]) {
+ initialValue = migrated[name]
+ }
+
+ useEffect(() => {
+ // ... so cleanup happens only after the render is committed
+ if (migrated[name]) {
+ // move to toMigrate for next refresh
+ toMigrate[name] = migrated[name]
+ delete migrated[name]
+ }
+ return () => {
+ // Remove migration on unmount
+ if (toMigrate[name]) {
+ delete toMigrate[name]
+ }
+ }
+ }, [name])
+
+ const [value, setValue] = useState(initialValue)
+
+ return [value, (value: S) => {
+ toMigrate[name] = value
+ setValue(value)
+ }]
+}
+
+export default Meteor.isClient
+ ? useMeteorState
+ : useState
diff --git a/packages/react-mongo/README.md b/packages/react-mongo/README.md
new file mode 100644
index 00000000..1877e6fd
--- /dev/null
+++ b/packages/react-mongo/README.md
@@ -0,0 +1,44 @@
+meteor/react-mongo
+==================
+
+```
+meteor add meteor/react-mongo
+```
+
+A set of hooks for using Meteor's Mini Mongo Collections and pub/sub.
+
+There are two hooks
+
+| Hook | Function
+| ---- | --------
+| useSubscription | Used to set up a Meteor subscription. In SSR, this hook is responsible for capturing data to be sent to the client for hydration.
+| useCursor | Manages the lifecycle of a Mongo Cursor
+
+Both methods accept a factory method, and deps.
+
+## useSubscription(factory, deps)
+
+`useSubscription` takes a factory method, which should return a subscription handle, and a deps array. It can also return `void` to conditionally set up no subscription. The hook returns a subscription handle with a reactive `ready` method.
+
+Invoking the `ready()` handle method will cause the hook to update react when the subscription becomes available.
+
+### Example
+
+```jsx
+import React from 'react'
+import { useSubscription } from 'meteor/react-mongo'
+
+const MyComponent = ({ id = null }) => {
+ const subscription = useSubscription(() => {
+ if (id) return Meteor.subscribe(id)
+ }, [id])
+
+ return
+ {
+ subscription.ready()
+ ? 'content ready'
+ : 'loading...'
+ }
+
+}
+```
diff --git a/packages/react-mongo/package-lock.json b/packages/react-mongo/package-lock.json
new file mode 100644
index 00000000..4647c678
--- /dev/null
+++ b/packages/react-mongo/package-lock.json
@@ -0,0 +1,430 @@
+{
+ "name": "react-mongo",
+ "requires": true,
+ "lockfileVersion": 1,
+ "dependencies": {
+ "@babel/code-frame": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz",
+ "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==",
+ "requires": {
+ "@babel/highlight": "^7.10.4"
+ }
+ },
+ "@babel/helper-validator-identifier": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz",
+ "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw=="
+ },
+ "@babel/highlight": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz",
+ "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==",
+ "requires": {
+ "@babel/helper-validator-identifier": "^7.10.4",
+ "chalk": "^2.0.0",
+ "js-tokens": "^4.0.0"
+ },
+ "dependencies": {
+ "chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "requires": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ }
+ }
+ }
+ },
+ "@babel/runtime": {
+ "version": "7.12.1",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.1.tgz",
+ "integrity": "sha512-J5AIf3vPj3UwXaAzb5j1xM4WAQDX3EMgemF8rjCP3SoW09LfRKAXQKt6CoVYl230P6iWdRcBbnLDDdnqWxZSCA==",
+ "requires": {
+ "regenerator-runtime": "^0.13.4"
+ }
+ },
+ "@babel/runtime-corejs3": {
+ "version": "7.12.1",
+ "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.12.1.tgz",
+ "integrity": "sha512-umhPIcMrlBZ2aTWlWjUseW9LjQKxi1dpFlQS8DzsxB//5K+u6GLTC/JliPKHsd5kJVPIU6X/Hy0YvWOYPcMxBw==",
+ "requires": {
+ "core-js-pure": "^3.0.0",
+ "regenerator-runtime": "^0.13.4"
+ }
+ },
+ "@jest/types": {
+ "version": "26.6.0",
+ "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.0.tgz",
+ "integrity": "sha512-8pDeq/JVyAYw7jBGU83v8RMYAkdrRxLG3BGnAJuqaQAUd6GWBmND2uyl+awI88+hit48suLoLjNFtR+ZXxWaYg==",
+ "requires": {
+ "@types/istanbul-lib-coverage": "^2.0.0",
+ "@types/istanbul-reports": "^3.0.0",
+ "@types/node": "*",
+ "@types/yargs": "^15.0.0",
+ "chalk": "^4.0.0"
+ }
+ },
+ "@testing-library/dom": {
+ "version": "7.26.3",
+ "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-7.26.3.tgz",
+ "integrity": "sha512-/1P6taENE/H12TofJaS3L1J28HnXx8ZFhc338+XPR5y1E3g5ttOgu86DsGnV9/n2iPrfJQVUZ8eiGYZGSxculw==",
+ "requires": {
+ "@babel/code-frame": "^7.10.4",
+ "@babel/runtime": "^7.10.3",
+ "@types/aria-query": "^4.2.0",
+ "aria-query": "^4.2.2",
+ "chalk": "^4.1.0",
+ "dom-accessibility-api": "^0.5.1",
+ "lz-string": "^1.4.4",
+ "pretty-format": "^26.4.2"
+ }
+ },
+ "@testing-library/react": {
+ "version": "10.4.9",
+ "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-10.4.9.tgz",
+ "integrity": "sha512-pHZKkqUy0tmiD81afs8xfiuseXfU/N7rAX3iKjeZYje86t9VaB0LrxYVa+OOsvkrveX5jCK3IjajVn2MbePvqA==",
+ "requires": {
+ "@babel/runtime": "^7.10.3",
+ "@testing-library/dom": "^7.22.3"
+ }
+ },
+ "@types/aria-query": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.0.tgz",
+ "integrity": "sha512-iIgQNzCm0v7QMhhe4Jjn9uRh+I6GoPmt03CbEtwx3ao8/EfoQcmgtqH4vQ5Db/lxiIGaWDv6nwvunuh0RyX0+A=="
+ },
+ "@types/bson": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/@types/bson/-/bson-4.0.3.tgz",
+ "integrity": "sha512-mVRvYnTOZJz3ccpxhr3wgxVmSeiYinW+zlzQz3SXWaJmD1DuL05Jeq7nKw3SnbKmbleW5qrLG5vdyWe/A9sXhw==",
+ "requires": {
+ "@types/node": "*"
+ }
+ },
+ "@types/connect": {
+ "version": "3.4.33",
+ "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.33.tgz",
+ "integrity": "sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A==",
+ "requires": {
+ "@types/node": "*"
+ }
+ },
+ "@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=="
+ },
+ "@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==",
+ "requires": {
+ "@types/istanbul-lib-coverage": "*"
+ }
+ },
+ "@types/istanbul-reports": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz",
+ "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==",
+ "requires": {
+ "@types/istanbul-lib-report": "*"
+ }
+ },
+ "@types/meteor": {
+ "version": "1.4.60",
+ "resolved": "https://registry.npmjs.org/@types/meteor/-/meteor-1.4.60.tgz",
+ "integrity": "sha512-NsuIIKtGABovJHrE2H0+PUDlGTuvCL3UjX9fgxJOk43oRzmA+1FMOnGz4n1n9J6G6vbw9PumdWZOWTZkH/NnRw==",
+ "requires": {
+ "@types/connect": "*",
+ "@types/mongodb": "*",
+ "@types/react": "*",
+ "@types/underscore": "*"
+ }
+ },
+ "@types/mongodb": {
+ "version": "3.5.29",
+ "resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-3.5.29.tgz",
+ "integrity": "sha512-aFEMjwSMJ6pstVDwDP4k7cys1iCbAGL01mAp5opOdbK9jv6JmwvYgpGje22Mp2HtrKq5Seea+5ti7CQ/Ovyw2Q==",
+ "requires": {
+ "@types/bson": "*",
+ "@types/node": "*"
+ }
+ },
+ "@types/node": {
+ "version": "14.14.0",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.0.tgz",
+ "integrity": "sha512-BfbIHP9IapdupGhq/hc+jT5dyiBVZ2DdeC5WwJWQWDb0GijQlzUFAeIQn/2GtvZcd2HVUU7An8felIICFTC2qg=="
+ },
+ "@types/prop-types": {
+ "version": "15.7.3",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz",
+ "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw=="
+ },
+ "@types/react": {
+ "version": "16.9.53",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.53.tgz",
+ "integrity": "sha512-4nW60Sd4L7+WMXH1D6jCdVftuW7j4Za6zdp6tJ33Rqv0nk1ZAmQKML9ZLD4H0dehA3FZxXR/GM8gXplf82oNGw==",
+ "requires": {
+ "@types/prop-types": "*",
+ "csstype": "^3.0.2"
+ }
+ },
+ "@types/underscore": {
+ "version": "1.10.24",
+ "resolved": "https://registry.npmjs.org/@types/underscore/-/underscore-1.10.24.tgz",
+ "integrity": "sha512-T3NQD8hXNW2sRsSbLNjF/aBo18MyJlbw0lSpQHB/eZZtScPdexN4HSa8cByYwTw9Wy7KuOFr81mlDQcQQaZ79w=="
+ },
+ "@types/yargs": {
+ "version": "15.0.9",
+ "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.9.tgz",
+ "integrity": "sha512-HmU8SeIRhZCWcnRskCs36Q1Q00KBV6Cqh/ora8WN1+22dY07AZdn6Gel8QZ3t26XYPImtcL8WV/eqjhVmMEw4g==",
+ "requires": {
+ "@types/yargs-parser": "*"
+ }
+ },
+ "@types/yargs-parser": {
+ "version": "15.0.0",
+ "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-15.0.0.tgz",
+ "integrity": "sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw=="
+ },
+ "ansi-regex": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
+ "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg=="
+ },
+ "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==",
+ "requires": {
+ "color-convert": "^1.9.0"
+ }
+ },
+ "aria-query": {
+ "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==",
+ "requires": {
+ "@babel/runtime": "^7.10.2",
+ "@babel/runtime-corejs3": "^7.10.2"
+ }
+ },
+ "chalk": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
+ "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
+ "requires": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "dependencies": {
+ "ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "requires": {
+ "color-convert": "^2.0.1"
+ }
+ },
+ "color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "requires": {
+ "color-name": "~1.1.4"
+ }
+ },
+ "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=="
+ },
+ "has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
+ },
+ "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==",
+ "requires": {
+ "has-flag": "^4.0.0"
+ }
+ }
+ }
+ },
+ "color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "requires": {
+ "color-name": "1.1.3"
+ }
+ },
+ "color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
+ },
+ "core-js-pure": {
+ "version": "3.6.5",
+ "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.6.5.tgz",
+ "integrity": "sha512-lacdXOimsiD0QyNf9BC/mxivNJ/ybBGJXQFKzRekp1WTHoVUWsUHEn+2T8GJAzzIhyOuXA+gOxCVN3l+5PLPUA=="
+ },
+ "csstype": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.3.tgz",
+ "integrity": "sha512-jPl+wbWPOWJ7SXsWyqGRk3lGecbar0Cb0OvZF/r/ZU011R4YqiRehgkQ9p4eQfo9DSDLqLL3wHwfxeJiuIsNag=="
+ },
+ "dom-accessibility-api": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.4.tgz",
+ "integrity": "sha512-TvrjBckDy2c6v6RLxPv5QXOnU+SmF9nBII5621Ve5fu6Z/BDrENurBEvlC1f44lKEUVqOpK4w9E5Idc5/EgkLQ=="
+ },
+ "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="
+ },
+ "has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
+ },
+ "js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
+ },
+ "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==",
+ "requires": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ }
+ },
+ "lz-string": {
+ "version": "1.4.4",
+ "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz",
+ "integrity": "sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY="
+ },
+ "object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
+ },
+ "pretty-format": {
+ "version": "26.6.0",
+ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.0.tgz",
+ "integrity": "sha512-Uumr9URVB7bm6SbaByXtx+zGlS+0loDkFMHP0kHahMjmfCtmFY03iqd++5v3Ld6iB5TocVXlBN/T+DXMn9d4BA==",
+ "requires": {
+ "@jest/types": "^26.6.0",
+ "ansi-regex": "^5.0.0",
+ "ansi-styles": "^4.0.0",
+ "react-is": "^16.12.0"
+ },
+ "dependencies": {
+ "ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "requires": {
+ "color-convert": "^2.0.1"
+ }
+ },
+ "color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "requires": {
+ "color-name": "~1.1.4"
+ }
+ },
+ "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=="
+ }
+ }
+ },
+ "prop-types": {
+ "version": "15.7.2",
+ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
+ "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==",
+ "requires": {
+ "loose-envify": "^1.4.0",
+ "object-assign": "^4.1.1",
+ "react-is": "^16.8.1"
+ }
+ },
+ "react": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react/-/react-16.13.1.tgz",
+ "integrity": "sha512-YMZQQq32xHLX0bz5Mnibv1/LHb3Sqzngu7xstSM+vrkE5Kzr9xE0yMByK5kMoTK30YVJE61WfbxIFFvfeDKT1w==",
+ "requires": {
+ "loose-envify": "^1.1.0",
+ "object-assign": "^4.1.1",
+ "prop-types": "^15.6.2"
+ }
+ },
+ "react-dom": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.13.1.tgz",
+ "integrity": "sha512-81PIMmVLnCNLO/fFOQxdQkvEq/+Hfpv24XNJfpyZhTRfO0QcmQIF/PgCa1zCOj2w1hrn12MFLyaJ/G0+Mxtfag==",
+ "requires": {
+ "loose-envify": "^1.1.0",
+ "object-assign": "^4.1.1",
+ "prop-types": "^15.6.2",
+ "scheduler": "^0.19.1"
+ }
+ },
+ "react-is": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
+ },
+ "react-test-renderer": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.13.1.tgz",
+ "integrity": "sha512-Sn2VRyOK2YJJldOqoh8Tn/lWQ+ZiKhyZTPtaO0Q6yNj+QDbmRkVFap6pZPy3YQk8DScRDfyqm/KxKYP9gCMRiQ==",
+ "requires": {
+ "object-assign": "^4.1.1",
+ "prop-types": "^15.6.2",
+ "react-is": "^16.8.6",
+ "scheduler": "^0.19.1"
+ }
+ },
+ "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=="
+ },
+ "scheduler": {
+ "version": "0.19.1",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz",
+ "integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==",
+ "requires": {
+ "loose-envify": "^1.1.0",
+ "object-assign": "^4.1.1"
+ }
+ },
+ "supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "requires": {
+ "has-flag": "^3.0.0"
+ }
+ },
+ "typescript": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.0.3.tgz",
+ "integrity": "sha512-tEu6DGxGgRJPb/mVPIZ48e69xCn2yRmCgYmDugAVwmJ6o+0u1RI18eO7E7WBTLYLaEVVOhwQmcdhQHweux/WPg=="
+ }
+ }
+}
diff --git a/packages/react-mongo/package.js b/packages/react-mongo/package.js
new file mode 100644
index 00000000..e62fb58d
--- /dev/null
+++ b/packages/react-mongo/package.js
@@ -0,0 +1,17 @@
+/* global Package */
+
+Package.describe({
+ name: 'react-mongo',
+ summary: 'React hook for reactively tracking Meteor data',
+ version: '0.9.0',
+ documentation: 'README.md',
+ git: 'https://github.com/meteor/react-packages',
+});
+
+Package.onUse(function (api) {
+ api.versionsFrom('1.10');
+ api.use('tracker');
+ api.use('typescript');
+
+ api.mainModule('react-mongo.ts', ['client', 'server'], { lazy: true });
+});
diff --git a/packages/react-mongo/package.json b/packages/react-mongo/package.json
new file mode 100644
index 00000000..6b836a62
--- /dev/null
+++ b/packages/react-mongo/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "meteor-react-mongo",
+ "scripts": {
+ "make-types": "npx typescript *.ts --declaration --emitDeclarationOnly --outDir types"
+ },
+ "dependencies": {
+ "@testing-library/react": "^10.0.2",
+ "@types/meteor": "^1.4.42",
+ "@types/react": "^16.9.34",
+ "react": "16.13.1",
+ "react-dom": "16.13.1",
+ "react-test-renderer": "16.13.1",
+ "typescript": "^4.0.3"
+ }
+}
diff --git a/packages/react-mongo/react-mongo.ts b/packages/react-mongo/react-mongo.ts
new file mode 100644
index 00000000..12534c3b
--- /dev/null
+++ b/packages/react-mongo/react-mongo.ts
@@ -0,0 +1,193 @@
+import { Meteor } from 'meteor/meteor'
+import { Mongo } from 'meteor/mongo'
+import { Tracker } from 'meteor/tracker'
+import { EJSON } from 'meteor/ejson'
+import { useEffect, useReducer, useRef, DependencyList, Reducer, useMemo } from 'react'
+
+const fur = (x: number): number => x + 1
+const useForceUpdate = () => useReducer(fur, 0)[1]
+
+type useSubscriptionRefs = {
+ subscription?: Meteor.SubscriptionHandle,
+ updateOnReady: boolean,
+ isReady: boolean,
+ params: {
+ name?: string,
+ args: any[]
+ }
+}
+
+const useSubscriptionClient = (name?: string, ...args: any[]): [() => boolean, Meteor.SubscriptionHandle | undefined] => {
+ const forceUpdate = useForceUpdate()
+
+ const refs: useSubscriptionRefs = useRef({
+ updateOnReady: false,
+ isReady: false,
+ params: {
+ name,
+ args
+ }
+ }).current
+
+ if (!EJSON.equals(refs.params, { name, args })) {
+ refs.updateOnReady = false
+ refs.isReady = false
+ refs.params = { name, args }
+ }
+
+ useEffect(() => {
+ const computation = Tracker.nonreactive(() => (
+ Tracker.autorun(() => {
+ const { name, args } = refs.params
+ if (!name) return
+
+ refs.subscription = Meteor.subscribe(name, ...args)
+
+ const isReady = refs.subscription.ready()
+ if (isReady !== refs.isReady) {
+ refs.isReady = isReady
+ if (refs.updateOnReady) {
+ forceUpdate()
+ }
+ }
+ })
+ ))
+
+ return () => {
+ computation.stop()
+ delete refs.subscription
+ }
+ }, [refs.params])
+
+ return [
+ () => {
+ refs.updateOnReady = true
+ return !refs.isReady
+ },
+ refs.subscription
+ ]
+}
+
+const useSubscriptionServer = (name?: string, ...args: any[]): [() => boolean, Meteor.SubscriptionHandle | undefined] => ([
+ () => false,
+ undefined
+])
+
+export const useSubscription = Meteor.isServer
+ ? useSubscriptionServer
+ : useSubscriptionClient
+
+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) => {
+ if (!(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, deps: DependencyList) => {
+ let [data, dispatch] = useReducer>>(
+ useFindReducer,
+ []
+ )
+
+ const cursor = useMemo(() => (
+ // To avoid creating side effects in render, opt out
+ // of Tracker integration altogether.
+ Tracker.nonreactive(() => {
+ const c = factory()
+ if (Meteor.isDevelopment) {
+ checkCursor(c)
+ }
+ data = c.fetch()
+ return c
+ })
+ ), deps)
+
+ useEffect(() => {
+ // 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 data
+}
+
+const useFindServer = (factory: () => Mongo.Cursor, deps: DependencyList) => (
+ Tracker.nonreactive(() => {
+ const cursor = factory()
+ if (Meteor.isDevelopment) checkCursor(cursor)
+ return cursor.fetch()
+ })
+)
+
+export const useFind = Meteor.isServer
+ ? useFindServer
+ : useFindClient
diff --git a/packages/react-mongo/types/react-mongo.d.ts b/packages/react-mongo/types/react-mongo.d.ts
new file mode 100644
index 00000000..c4d38593
--- /dev/null
+++ b/packages/react-mongo/types/react-mongo.d.ts
@@ -0,0 +1,5 @@
+import { Meteor } from 'meteor/meteor';
+import { Mongo } from 'meteor/mongo';
+import { DependencyList } from 'react';
+export declare const useSubscription: (name?: string, ...args: any[]) => [() => boolean, Meteor.SubscriptionHandle | undefined];
+export declare const useFind: (factory: () => Mongo.Cursor, deps: DependencyList) => T[];