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