Skip to content

Commit

Permalink
Replace Mixin of createContainer with Higher-Order Component
Browse files Browse the repository at this point in the history
* use high-order component to replace mixin

* fix undefined error

* Rename MeteorDataManger.js to MeteorDataManager.js

* Update createContainer.js

* Update MeteorDataManager.js
  • Loading branch information
pomelyu authored and charpeni committed May 24, 2018
1 parent 905f572 commit b49a1d2
Show file tree
Hide file tree
Showing 5 changed files with 195 additions and 35 deletions.
2 changes: 1 addition & 1 deletion lib/mongo-id.js
Original file line number Diff line number Diff line change
@@ -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]*$/);
Expand Down
15 changes: 6 additions & 9 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
116 changes: 116 additions & 0 deletions src/components/MeteorDataManager.js
Original file line number Diff line number Diff line change
@@ -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;
96 changes: 72 additions & 24 deletions src/components/createContainer.js
Original file line number Diff line number Diff line change
@@ -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 <Component {...this.props} {...this.data} />;
},
});
return <WrappedComponent { ...this.props } { ...this.data } />;
}
}

const newDisplayName = WrappedComponent.displayName || WrappedComponent.name || 'Component';
componentWithMeteorContainer.displayName = `WithMeteorContainer(${newDisplayName})`;

return componentWithMeteorContainer;
}

0 comments on commit b49a1d2

Please sign in to comment.