From b49a1d27d85ded41b8c7c191c76e3661686cf879 Mon Sep 17 00:00:00 2001 From: Chien Chin-yu Date: Fri, 25 May 2018 02:27:37 +0800 Subject: [PATCH] Replace Mixin of createContainer with Higher-Order Component * use high-order component to replace mixin * fix undefined error * Rename MeteorDataManger.js to MeteorDataManager.js * Update createContainer.js * Update MeteorDataManager.js --- lib/mongo-id.js | 2 +- package-lock.json | 15 ++-- package.json | 1 - src/components/MeteorDataManager.js | 116 ++++++++++++++++++++++++++++ src/components/createContainer.js | 96 +++++++++++++++++------ 5 files changed, 195 insertions(+), 35 deletions(-) create mode 100644 src/components/MeteorDataManager.js diff --git a/lib/mongo-id.js b/lib/mongo-id.js index 385da09..24c91bd 100644 --- a/lib/mongo-id.js +++ b/lib/mongo-id.js @@ -1,7 +1,7 @@ //https://github.com/meteor/meteor/tree/master/packages/mongo-id import EJSON from "ejson"; -MongoID = {}; +const MongoID = {}; MongoID._looksLikeObjectID = function (str) { return str.length === 24 && str.match(/^[0-9a-f]*$/); diff --git a/package-lock.json b/package-lock.json index 34947e0..c1608d8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1093,15 +1093,12 @@ "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz", "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=" }, - "create-react-class": { - "version": "15.6.2", - "resolved": "https://registry.npmjs.org/create-react-class/-/create-react-class-15.6.2.tgz", - "integrity": "sha1-zx7RXxKq1/FO9fLf4F5sQvke8Co=", - "requires": { - "fbjs": "0.8.16", - "loose-envify": "1.3.1", - "object-assign": "4.1.1" - } + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true, + "optional": true }, "crypto-js": { "version": "3.1.8", diff --git a/package.json b/package.json index 5f592db..7347b85 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,6 @@ "homepage": "https://github.com/inProgress-team/react-native-meteor#readme", "dependencies": { "base-64": "^0.1.0", - "create-react-class": "^15.6.0", "crypto-js": "^3.1.6", "ejson": "^2.1.2", "minimongo-cache": "0.0.48", diff --git a/src/components/MeteorDataManager.js b/src/components/MeteorDataManager.js new file mode 100644 index 0000000..9336d8a --- /dev/null +++ b/src/components/MeteorDataManager.js @@ -0,0 +1,116 @@ +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/createContainer.js b/src/components/createContainer.js index 040df15..c649bb5 100644 --- a/src/components/createContainer.js +++ b/src/components/createContainer.js @@ -1,32 +1,80 @@ -/** - * Container helper using react-meteor-data. - */ - import React from 'react'; -import createReactClass from 'create-react-class'; +import EJSON from 'ejson'; -import Mixin from './Mixin'; +import Data from '../Data'; +import MeteorDataManager from './MeteorDataManager'; -export default function createContainer(options = {}, Component) { - let expandedOptions = options; - if (typeof options === 'function') { - expandedOptions = { - getMeteorData: options, - }; - } +export default function createContainer(mapMeteorDataToProps, WrappedComponent) { + class componentWithMeteorContainer extends React.Component { + constructor(props) { + super(props); - const { - getMeteorData - } = expandedOptions; + this.getMeteorData = this.getMeteorData.bind(this); + } - return createReactClass({ - displayName: 'MeteorDataContainer', - mixins: [Mixin], getMeteorData() { - return getMeteorData(this.props); - }, + return mapMeteorDataToProps(this.props); + } + + componentWillMount() { + Data.waitDdpReady(() => { + if (this.getMeteorData) { + this.data = {}; + this._meteorDataManager = new MeteorDataManager(this); + const newData = this._meteorDataManager.calculateData(); + this._meteorDataManager.updateData(newData); + } + }); + } + + 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(); + } + } + render() { - return ; - }, - }); + return ; + } + } + + const newDisplayName = WrappedComponent.displayName || WrappedComponent.name || 'Component'; + componentWithMeteorContainer.displayName = `WithMeteorContainer(${newDisplayName})`; + + return componentWithMeteorContainer; }