diff --git a/src/Meteor.js b/src/Meteor.js
index b4a4ed8..566c3c0 100644
--- a/src/Meteor.js
+++ b/src/Meteor.js
@@ -16,7 +16,8 @@ import Mongo from './Mongo';
import { Collection, runObservers, localCollections } from './Collection';
import call from './Call';
-import withTracker from './components/ReactMeteorData';
+import withTracker from './components/withTracker';
+import useTracker from './components/useTracker';
import ReactiveDict from './ReactiveDict';
@@ -42,6 +43,7 @@ module.exports = {
return new Collection(name, options);
},
withTracker,
+ useTracker,
getData() {
return Data;
},
@@ -84,7 +86,7 @@ module.exports = {
if((!endpoint.startsWith("ws") || !endpoint.endsWith("/websocket")) && !options.suppressUrlErrors) {
throw new Error(`Your url "${endpoint}" may be in the wrong format. It should start with "ws://" or "wss://" and end with "/websocket", e.g. "wss://myapp.meteor.com/websocket". To disable this warning, connect with option "suppressUrlErrors" as true, e.g. Meteor.connect("${endpoint}", {suppressUrlErrors:true});`);
}
-
+
if (!options.AsyncStorage) {
const AsyncStorage = require('@react-native-community/async-storage').default;
@@ -155,9 +157,9 @@ module.exports = {
_id: message.id,
...message.fields,
};
-
+
Data.db[message.collection].upsert(document);
-
+
runObservers("added", message.collection, document);
});
@@ -192,18 +194,18 @@ module.exports = {
...message.fields,
...unset,
};
-
+
const oldDocument = Data.db[message.collection].findOne({_id:message.id});
-
+
Data.db[message.collection].upsert(document);
-
- runObservers("changed", message.collection, document, oldDocument);
+
+ runObservers("changed", message.collection, document, oldDocument);
}
});
Data.ddp.on('removed', message => {
if(Data.db[message.collection]) {
- const oldDocument = Data.db[message.collection].findOne({_id:message.id});
+ const oldDocument = Data.db[message.collection].findOne({_id:message.id});
Data.db[message.collection].del(message.id);
runObservers("removed", message.collection, oldDocument);
}
diff --git a/src/components/MeteorDataManager.js b/src/components/MeteorDataManager.js
deleted file mode 100644
index c6b75bd..0000000
--- a/src/components/MeteorDataManager.js
+++ /dev/null
@@ -1,118 +0,0 @@
-import Trackr from 'trackr';
-import Data from '../Data';
-
-// A class to keep the state and utility methods needed to manage
-// the Meteor data for a component.
-class MeteorDataManager {
- constructor(component) {
- this.component = component;
- this.computation = null;
- this.oldData = null;
- this._meteorDataDep = new Trackr.Dependency();
- this._meteorDataChangedCallback = () => {
- this._meteorDataDep.changed();
- };
-
- Data.onChange(this._meteorDataChangedCallback);
- }
-
- dispose() {
- if (this.computation) {
- this.computation.stop();
- this.computation = null;
- }
-
- Data.offChange(this._meteorDataChangedCallback);
- }
-
- calculateData() {
- const component = this.component;
-
- if (!component.getMeteorData) {
- return null;
- }
-
- if (this.computation) {
- this.computation.stop();
- this.computation = null;
- }
-
- let data;
- // 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
- // Computations, where if the outer one is invalidated or stopped,
- // it stops the inner one.
-
- this.computation = Trackr.nonreactive(() => {
- return Trackr.autorun(c => {
- this._meteorDataDep.depend();
- if (c.firstRun) {
- const savedSetState = component.setState;
- try {
- component.setState = () => {
- throw new Error(
- "Can't call `setState` inside `getMeteorData` as this could cause an endless" +
- ' loop. To respond to Meteor data changing, consider making this component' +
- ' a "wrapper component" that only fetches data and passes it in as props to' +
- ' a child component. Then you can use `componentWillReceiveProps` in that' +
- ' child component.'
- );
- };
-
- data = component.getMeteorData();
- } finally {
- component.setState = savedSetState;
- }
- } else {
- // Stop this computation instead of using the re-run.
- // We use a brand-new autorun for each call to getMeteorData
- // to capture dependencies on any reactive data sources that
- // are accessed. The reason we can't use a single autorun
- // for the lifetime of the component is that Tracker only
- // re-runs autoruns at flush time, while we need to be able to
- // re-call getMeteorData synchronously whenever we want, e.g.
- // from componentWillUpdate.
- c.stop();
- // Calling forceUpdate() triggers componentWillUpdate which
- // recalculates getMeteorData() and re-renders the component.
- try {
- component.forceUpdate();
- } catch (e) {
- console.error(e);
- }
- }
- });
- });
-
- return data;
- }
-
- updateData(newData) {
- const component = this.component;
- const oldData = this.oldData;
-
- if (!(newData && typeof newData === 'object')) {
- throw new Error('Expected object returned from getMeteorData');
- }
- // update componentData in place based on newData
- for (let key in newData) {
- component.data[key] = newData[key];
- }
- // if there is oldData (which is every time this method is called
- // except the first), delete keys in newData that aren't in
- // oldData. don't interfere with other keys, in case we are
- // co-existing with something else that writes to a component's
- // this.data.
- if (oldData) {
- for (let key in oldData) {
- if (!(key in newData)) {
- delete component.data[key];
- }
- }
- }
- this.oldData = newData;
- }
-}
-
-export default MeteorDataManager;
diff --git a/src/components/ReactMeteorData.js b/src/components/ReactMeteorData.js
deleted file mode 100644
index cfce2ff..0000000
--- a/src/components/ReactMeteorData.js
+++ /dev/null
@@ -1,97 +0,0 @@
-import React from 'react';
-import EJSON from 'ejson';
-
-import Data from '../Data';
-import MeteorDataManager from './MeteorDataManager';
-
-const ReactMeteorData = {
-
-
- UNSAFE_componentWillUpdate(nextProps, nextState) {
- if (this.startMeteorSubscriptions) {
- if (
- !EJSON.equals(this.state, nextState) ||
- !EJSON.equals(this.props, nextProps)
- ) {
- this._meteorSubscriptionsManager._meteorDataChangedCallback();
- }
- }
-
- if (this.getMeteorData) {
- const saveProps = this.props;
- const saveState = this.state;
- let newData;
- try {
- // Temporarily assign this.state and this.props,
- // so that they are seen by getMeteorData!
- // This is a simulation of how the proposed Observe API
- // for React will work, which calls observe() after
- // componentWillUpdate and after props and state are
- // updated, but before render() is called.
- // See https://github.com/facebook/react/issues/3398.
- this.props = nextProps;
- this.state = nextState;
- newData = this._meteorDataManager.calculateData();
- } finally {
- this.props = saveProps;
- this.state = saveState;
- }
-
- this._meteorDataManager.updateData(newData);
- }
- },
-
- componentWillUnmount() {
- if (this._meteorDataManager) {
- this._meteorDataManager.dispose();
- }
-
- if (this._meteorSubscriptionsManager) {
- this._meteorSubscriptionsManager.dispose();
- }
- },
-};
-
-export { ReactMeteorData };
-
-class ReactComponent extends React.Component {}
-Object.assign(ReactComponent.prototype, ReactMeteorData);
-
-class ReactPureComponent extends React.PureComponent {}
-Object.assign(ReactPureComponent.prototype, ReactMeteorData);
-
-export default function connect(options) {
- let expandedOptions = options;
- if (typeof options === 'function') {
- expandedOptions = {
- getMeteorData: options,
- };
- }
-
- const { getMeteorData, pure = true } = expandedOptions;
-
- const BaseComponent = pure ? ReactPureComponent : ReactComponent;
- return WrappedComponent =>
- class ReactMeteorDataComponent extends BaseComponent {
- constructor(props) {
- super(props);
-
- Data.waitDdpReady(() => {
- if (this.getMeteorData) {
- this.data = {};
- this._meteorDataManager = new MeteorDataManager(this);
- const newData = this._meteorDataManager.calculateData();
- this._meteorDataManager.updateData(newData);
- }
- });
- }
-
- getMeteorData() {
- return getMeteorData(this.props);
- }
-
- render() {
- return ;
- }
- };
-}
diff --git a/src/components/useTracker.js b/src/components/useTracker.js
new file mode 100644
index 0000000..b32a510
--- /dev/null
+++ b/src/components/useTracker.js
@@ -0,0 +1,30 @@
+import { useEffect, useState } from 'react';
+import Tracker from 'trackr';
+import Data from '../Data';
+
+export default (trackerFn, deps = []) => {
+ const [response, setResponse] = useState(trackerFn());
+ const meteorDataDep = new Tracker.Dependency();
+ let computation = null;
+ const dataChangedCallback = () => {
+ meteorDataDep.changed();
+ };
+
+ const stopComputation = () => {
+ computation && computation.stop();
+ computation = null;
+ };
+
+ Data.onChange(dataChangedCallback);
+
+ useEffect(() => {
+ stopComputation();
+ Tracker.autorun(currentComputation => {
+ meteorDataDep.depend();
+ computation = currentComputation;
+ setResponse(trackerFn());
+ });
+ return () => { stopComputation(); Data.offChange(dataChangedCallback); };
+ }, deps);
+ return response;
+};
diff --git a/src/components/withTracker.js b/src/components/withTracker.js
new file mode 100644
index 0000000..749e15b
--- /dev/null
+++ b/src/components/withTracker.js
@@ -0,0 +1,16 @@
+import React, { forwardRef, memo } from 'react';
+import useTracker from './useTracker';
+
+export default function withTracker (options) {
+ return Component => {
+ const expandedOptions = typeof options === 'function' ? { getMeteorData: options } : options;
+ const { getMeteorData, pure = true } = expandedOptions;
+
+ const WithTracker = forwardRef((props, ref) => {
+ const data = useTracker(() => getMeteorData(props) || {});
+ return ;
+ });
+
+ return pure ? memo(WithTracker) : WithTracker;
+ };
+}