diff --git a/packages/react-meteor-data/useTracker.tests.js b/packages/react-meteor-data/useTracker.tests.js
index ea04cd80..a1e37a21 100644
--- a/packages/react-meteor-data/useTracker.tests.js
+++ b/packages/react-meteor-data/useTracker.tests.js
@@ -22,22 +22,12 @@ if (Meteor.isClient) {
const reactiveDict = new ReactiveDict();
let runCount = 0;
- let computation;
- let createdCount = 0;
- let destroyedCount = 0;
let value;
const Test = () => {
value = useTracker(() => {
runCount++;
reactiveDict.setDefault('key', 'initial');
return reactiveDict.get('key');
- }, null, (c) => {
- test.isFalse(c === computation, 'The new computation should always be a new instance');
- computation = c;
- createdCount++;
- return () => {
- destroyedCount++;
- }
});
return {value};
};
@@ -63,10 +53,6 @@ if (Meteor.isClient) {
test.equal(value, 'initial', 'Expect initial value to be "initial"');
test.equal(runCount, 2 * strictMul, 'Should have run 2 times - first, and in useEffect');
- if (Meteor.isClient) {
- test.equal(createdCount, 2 * strictMul, 'Should have been created 2 times');
- test.equal(destroyedCount, 2 * strictMul - 1, 'Should have been destroyed 1 time');
- }
await waitFor(() => {
reactiveDict.set('key', 'changed');
@@ -75,10 +61,6 @@ if (Meteor.isClient) {
test.equal(value, 'changed', 'Expect new value to be "changed"');
test.equal(runCount, 3 * strictMul, 'Should have run 3 times');
- if (Meteor.isClient) {
- test.equal(createdCount, 3 * strictMul, 'Should have been created 3 times');
- test.equal(destroyedCount, 3 * strictMul - 1, 'Should have been destroyed 1 less than created');
- }
await waitFor(() => {
rerender();
@@ -86,20 +68,12 @@ if (Meteor.isClient) {
test.equal(value, 'changed', 'Expect value of "changed" to persist after rerender');
test.equal(runCount, 4 * strictMul, 'Should have run 4 times');
- if (Meteor.isClient) {
- test.equal(createdCount, 4 * strictMul, 'Should have been created 4 times');
- test.equal(destroyedCount, 4 * strictMul -1, 'Should have been destroyed 1 less than created');
- }
await waitFor(() => {
unmount();
}, { container, timeout: 250 });
test.equal(runCount, 4 * strictMul, 'Unmount should not cause a tracker run');
- if (Meteor.isClient) {
- test.equal(createdCount, 4 * strictMul, 'Should have been created 4 times');
- test.equal(destroyedCount, 4 * strictMul, 'Should have been destroyed the same number of times as created');
- }
await waitFor(() => {
reactiveDict.set('different', 'changed again');
@@ -108,20 +82,16 @@ if (Meteor.isClient) {
test.equal(value, 'changed', 'After unmount, changes to the reactive source should not update the value.');
test.equal(runCount, 4 * strictMul, 'After unmount, useTracker should no longer be tracking');
- if (Meteor.isClient) {
- test.equal(createdCount, 4 * strictMul, 'Should have been created 4 times');
- test.equal(destroyedCount, 4 * strictMul, 'Should have been destroyed the same number of times as created');
- }
reactiveDict.destroy();
};
- Tinytest.addAsync('useTracker - no deps', async function (test, completed) {
+ Tinytest.addAsync('useTracker (no deps) - Normal', async function (test, completed) {
await noDepsTester(test);
completed();
});
- Tinytest.addAsync('useTracker - no deps in StrictMode', async function (test, completed) {
+ Tinytest.addAsync('useTracker (no deps) - in StrictMode', async function (test, completed) {
await noDepsTester(test, 'strict-mode');
completed();
});
@@ -131,23 +101,13 @@ if (Meteor.isClient) {
const reactiveDict = new ReactiveDict();
let runCount = 0;
- let computation;
- let createdCount = 0;
- let destroyedCount = 0;
let value;
const Test = ({ name }) => {
value = useTracker(() => {
runCount++;
reactiveDict.setDefault(name, 'initial');
return reactiveDict.get(name);
- }, [name], (c) => {
- test.isFalse(c === computation, 'The new computation should always be a new instance');
- computation = c;
- createdCount++;
- return () => {
- destroyedCount++;
- }
- });
+ }, [name]);
return {value};
};
@@ -182,11 +142,7 @@ if (Meteor.isClient) {
await waitFor(() => {}, { container, timeout: 250 });
test.equal(value, 'initial', 'Expect the initial value for given name to be "initial"');
- test.equal(runCount, 1 + strictAdd, 'Should have run 1 times - still only the sync invocation (unlike without deps)');
- if (Meteor.isClient) {
- test.equal(createdCount, 1 + strictAdd, 'Should have been created 1 times');
- test.equal(destroyedCount, 0, 'Should not have been destroyed yet');
- }
+ test.equal(runCount, 2 + strictAdd, 'Should have run 2 times');
await waitFor(() => {
reactiveDict.set('name', 'changed');
@@ -194,22 +150,14 @@ if (Meteor.isClient) {
}, { container, timeout: 250 });
test.equal(value, 'changed', 'Expect the new value for given name to be "changed"');
- test.equal(runCount, 2 + strictAdd, 'Should have run 2 times after reactive change');
- if (Meteor.isClient) {
- test.equal(createdCount, 1 + strictAdd, 'Should have been created 1 times');
- test.equal(destroyedCount, 1 + strictAdd - 1, 'Should not have been destroyed yet');
- }
+ test.equal(runCount, 3 + strictAdd, 'Should have run 3 times after reactive change');
await waitFor(() => {
rerender();
}, { container, timeout: 250 });
test.equal(value, 'changed', 'Expect the new value "changed" for given name to have persisted through render');
- test.equal(runCount, 2 + strictAdd, 'Should still have run only 2 times');
- if (Meteor.isClient) {
- test.equal(createdCount, 1 + strictAdd, 'Should have been created 1 times');
- test.equal(destroyedCount, 1 + strictAdd - 1, 'Should not have been destroyed yet');
- }
+ test.equal(runCount, 3 + strictAdd, 'Should still have run only 3 times');
await waitFor(() => {
rerender('different');
@@ -217,36 +165,27 @@ if (Meteor.isClient) {
if (mode === 'strict-mode') strictAdd++;
test.equal(value, 'initial', 'After deps change, the initial value should have returned');
- test.equal(runCount, 3 + strictAdd, 'Should have run 3 times');
- if (Meteor.isClient) {
- test.equal(createdCount, 2 + strictAdd, 'Should have been created 2 times');
- test.equal(destroyedCount, 2 + strictAdd - 1, 'Should have been destroyed 1 times');
- }
+ test.equal(runCount, 5 + strictAdd, 'Should have run 5 times');
await waitFor(() => {
unmount();
}, { container, timeout: 250 });
- test.equal(runCount, 3 + strictAdd, 'Unmount should not cause a tracker run');
- if (Meteor.isClient) {
- test.equal(createdCount, 2 + strictAdd, 'Should have been created 2 times');
- test.equal(destroyedCount, 2 + strictAdd, 'Should have been destroyed 2 times');
- }
-
+ test.equal(runCount, 5 + strictAdd, 'Unmount should not cause a tracker run');
reactiveDict.destroy();
};
- Tinytest.addAsync('useTracker - with deps', async function (test, completed) {
+ Tinytest.addAsync('useTracker - Normal', async function (test, completed) {
await depsTester(test);
completed();
});
- Tinytest.addAsync('useTracker - with deps in StrictMode', async function (test, completed) {
+ Tinytest.addAsync('useTracker - in StrictMode', async function (test, completed) {
await depsTester(test, 'strict-mode');
completed();
});
- Tinytest.addAsync('useTracker - basic track', async function (test, completed) {
+ Tinytest.addAsync('useTracker (no deps) - basic track', async function (test, completed) {
var container = document.createElement("DIV");
var x = new ReactiveVar('aaa');
@@ -281,12 +220,47 @@ if (Meteor.isClient) {
completed();
});
+ Tinytest.addAsync('useTracker - basic track', async function (test, completed) {
+ var container = document.createElement("DIV");
+
+ var x = new ReactiveVar('aaa');
+
+ var Foo = () => {
+ const data = useTracker(() => {
+ return {
+ x: x.get()
+ };
+ }, []);
+ return {data.x};
+ };
+
+ ReactDOM.render(, container);
+ test.equal(getInnerHtml(container), 'aaa');
+
+ x.set('bbb');
+ await waitFor(() => {
+ Tracker.flush({_throwFirstError: true});
+ }, { container, timeout: 250 });
+
+ test.equal(getInnerHtml(container), 'bbb');
+
+ test.equal(x._numListeners(), 1);
+
+ await waitFor(() => {
+ ReactDOM.unmountComponentAtNode(container);
+ }, { container, timeout: 250 });
+
+ test.equal(x._numListeners(), 0);
+
+ completed();
+ });
+
// Make sure that calling ReactDOM.render() from an autorun doesn't
// associate that autorun with the mixin's autorun. When autoruns are
// nested, invalidating the outer one stops the inner one, unless
// Tracker.nonreactive is used. This test tests for the use of
// Tracker.nonreactive around the mixin's autorun.
- Tinytest.addAsync('useTracker - render in autorun', async function (test, completed) {
+ Tinytest.addAsync('useTracker (no deps) - render in autorun', async function (test, completed) {
var container = document.createElement("DIV");
var x = new ReactiveVar('aaa');
@@ -318,7 +292,45 @@ if (Meteor.isClient) {
completed();
});
- Tinytest.addAsync('useTracker - track based on props and state', async function (test, completed) {
+
+ // Make sure that calling ReactDOM.render() from an autorun doesn't
+ // associate that autorun with the mixin's autorun. When autoruns are
+ // nested, invalidating the outer one stops the inner one, unless
+ // Tracker.nonreactive is used. This test tests for the use of
+ // Tracker.nonreactive around the mixin's autorun.
+ Tinytest.addAsync('useTracker - render in autorun', async function (test, completed) {
+ var container = document.createElement("DIV");
+
+ var x = new ReactiveVar('aaa');
+
+ var Foo = () => {
+ const data = useTracker(() => {
+ return {
+ x: x.get()
+ };
+ }, []);
+ return {data.x};
+ };
+
+ Tracker.autorun(function (c) {
+ ReactDOM.render(, container);
+ // Stopping this autorun should not affect the mixin's autorun.
+ c.stop();
+ });
+ test.equal(getInnerHtml(container), 'aaa');
+
+ x.set('bbb');
+ await waitFor(() => {
+ Tracker.flush({_throwFirstError: true});
+ }, { container, timeout: 250 });
+ test.equal(getInnerHtml(container), 'bbb');
+
+ ReactDOM.unmountComponentAtNode(container);
+
+ completed();
+ });
+
+ Tinytest.addAsync('useTracker (no deps) - track based on props and state', async function (test, completed) {
var container = document.createElement("DIV");
var xs = [new ReactiveVar('aaa'),
@@ -387,7 +399,7 @@ if (Meteor.isClient) {
completed();
});
- Tinytest.addAsync('useTracker - track based on props and state (with deps)', async function (test, completed) {
+ Tinytest.addAsync('useTracker - track based on props and state', async function (test, completed) {
var container = document.createElement("DIV");
var xs = [new ReactiveVar('aaa'),
@@ -457,6 +469,111 @@ if (Meteor.isClient) {
});
};
+ testAsyncMulti('useTracker (no deps) - resubscribe', [
+ function (test, expect) {
+ var self = this;
+ self.div = document.createElement("DIV");
+ self.collection = new Mongo.Collection("useTrackerLegacy-mixin-coll");
+ self.num = new ReactiveVar(1);
+ self.someOtherVar = new ReactiveVar('foo');
+ self.Foo = () => {
+ const data = useTracker(() => {
+ self.handle =
+ Meteor.subscribe("useTrackerLegacy-mixin-sub",
+ self.num.get());
+
+ return {
+ v: self.someOtherVar.get(),
+ docs: self.collection.find().fetch()
+ };
+ });
+ self.data = data;
+ return
{
+ _.map(data.docs, (doc) => {doc._id})
+ }
;
+ };
+
+ self.component = ReactDOM.render(, self.div);
+ test.equal(getInnerHtml(self.div), '', 'div should be empty');
+
+ var handle = self.handle;
+ test.isFalse(handle.ready(), 'handle.ready() should be false');
+
+ waitForTracker(() => handle.ready(),
+ expect());
+ },
+ function (test, expect) {
+ var self = this;
+ 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;
+
+ // can't call Tracker.flush() here (we are in a Tracker.flush already)
+ Tracker.afterFlush(expect());
+ },
+ function (test, expect) {
+ var self = this;
+ var oldHandle = self.oldHandle1;
+ var newHandle = self.handle;
+ 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
', 'div should contain id1');
+
+ // ok, now change the `num` argument to the subscription
+ self.num.set(2);
+ self.oldHandle2 = newHandle;
+ Tracker.afterFlush(expect());
+ },
+ function (test, expect) {
+ var self = this;
+ // data is still there
+ test.equal(getInnerHtml(self.div), 'id1
', 'div shold contain id1');
+ // handle is no longer ready
+ var handle = self.handle;
+ test.isFalse(handle.ready(), 'handle.ready() should be false');
+ // different sub ID
+ 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());
+ },
+ function (test, expect) {
+ var self = this;
+ // now we see the new data! (and maybe the old data, because
+ // when a subscription goes away, its data doesn't disappear right
+ // away; the server has to tell the client which documents or which
+ // properties to remove, and this is not easy to wait for either; see
+ // https://github.com/meteor/meteor/issues/2440)
+ test.equal(getInnerHtml(self.div).replace('id1', ''),
+ 'id2
');
+
+ self.someOtherVar.set('baz');
+ self.oldHandle3 = self.handle;
+
+ Tracker.afterFlush(expect());
+ },
+ function (test, expect) {
+ var self = this;
+ 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, 'same for subscriptionId');
+ test.isTrue(self.handle.ready(), 'self.handle.ready() should be true');
+ },
+ function (test, expect) {
+ ReactDOM.unmountComponentAtNode(this.div);
+ // break out of flush time, so we don't call the test's
+ // onComplete from within Tracker.flush
+ Meteor.defer(expect());
+ }
+ ]);
+
testAsyncMulti('useTracker - resubscribe', [
function (test, expect) {
var self = this;
@@ -474,7 +591,7 @@ if (Meteor.isClient) {
v: self.someOtherVar.get(),
docs: self.collection.find().fetch()
};
- });
+ }, []);
self.data = data;
return {
_.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..bea96a0c 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,31 @@ 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;
+interface IReactiveFn
{
+ (c?: Tracker.Computation): T
+}
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: IReactiveFn) => {
+ 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;
- }
-};
-
-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;
+ // @ts-ignore This makes TS think ref.computation is "never" set
+ delete refs.computation;
}
-};
-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 +61,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 +80,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,80 +95,51 @@ 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();
-
- // Always have up to date deps and computations in all contexts
- refs.reactiveFn = reactiveFn;
- refs.deps = deps;
- if (computationHandler) {
- refs.computationHandler = computationHandler;
- }
+const useTrackerWithDeps = (reactiveFn: IReactiveFn, deps: DependencyList): T => {
+ let [data, setData] = useState();
- // 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) =>
+const useTrackerClient = (reactiveFn: IReactiveFn, deps: DependencyList = null): T =>
(deps === null || deps === undefined || !Array.isArray(deps))
- ? useTrackerNoDeps(reactiveFn, deps, computationHandler)
- : useTrackerWithDeps(reactiveFn, deps, computationHandler);
+ ? useTrackerNoDeps(reactiveFn)
+ : useTrackerWithDeps(reactiveFn, deps);
-const useTrackerServer: useTrackerSignature = (reactiveFn, deps = null, computationHandler) =>
- Tracker.nonreactive(reactiveFn);
+const useTrackerServer = (reactiveFn: IReactiveFn, deps: DependencyList): T =>
+ Tracker.nonreactive(reactiveFn) as T;
// When rendering on the server, we don't want to use the Tracker.
// We only do the first rendering on the server so we can get the data right away
@@ -259,26 +147,20 @@ const useTracker = Meteor.isServer
? useTrackerServer
: useTrackerClient;
-const useTrackerDev: useTrackerSignature = (reactiveFn, deps = null, computationHandler) => {
+const useTrackerDev = (reactiveFn: IReactiveFn, 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