mobxjs / mobx

Simple, scalable state management.
http://mobx.js.org
MIT License
27.47k stars 1.77k forks source link

How integrate with Meteor Tracker example? #84

Closed bySabi closed 8 years ago

bySabi commented 8 years ago

Hello @mweststrate.

I'm trying to use/integrate mobservable with Meteor.

Commented code is tested and working. This a sample of current proof of concept code:

var afiliado = mobservable.observable(function() {
    var afiliado;
    Tracker.autorun(function() {
      // HARDCODE MongoID for test only
      afiliado = Orgz.collections.Afiliados.findOne({_id: 'EbPM2uWJhbd8rZP8P'});
    });
    return afiliado;
});

ViewPersona = mobservableReact.observer(React.createClass({
/*
  mixins: [ReactMeteorData],
  getMeteorData() {
    Meteor.subscribe("afiliados", this.props.id);
    return {afiliado: Orgz.collections.Afiliados.findOne(this.props.id)};
  },
*/
  render() {
  //  return <ViewPersona_ afiliado={this.data.afiliado} />;
    return <ViewPersona_ afiliado={afiliado} />;
  }
}));

ViewPersona_ = React.createClass({
  render() {
    return <AfiliadoTabs afiliado={this.props.afiliado} />;
  }
});

"mobviously" I don´t get a plain object on afiliado var, just: ComputedObservable[[m#1] (current value:'undefined')]

is possible do this ? what is the way?

Thanks for your time and happy new year!!!

bySabi commented 8 years ago

Hi @mweststrate

Is working with a few "mobviously" :-) sorry!, changes ... on line: return <ViewPersona_ afiliado={afiliado()} />;

var afiliado = mobservable.observable(function() {
    var afiliado;
    Tracker.autorun(function() {
      // HARDCODE MongoID for test only
      afiliado = Orgz.collections.Afiliados.findOne({_id: 'EbPM2uWJhbd8rZP8P'});
    });
    return afiliado;
});

ViewPersona = mobservableReact.observer(React.createClass({
/*
  mixins: [ReactMeteorData],
  getMeteorData() {
    Meteor.subscribe("afiliados", this.props.id);
    return {afiliado: Orgz.collections.Afiliados.findOne(this.props.id)};
  },
*/
  render() {
  //  return <ViewPersona_ afiliado={this.data.afiliado} />;
    return <ViewPersona_ afiliado={afiliado()} />;
  }
}));

ViewPersona_ = React.createClass({
  render() {
    return <AfiliadoTabs afiliado={this.props.afiliado} />;
  }
});

A few questions: 1- This Tracker/moboservable have sense? 2- You see any problem with this solution? 3- observable function afiliado return must be: return afiliado ? afiliado : null; is working with return afiliado

I going to deep more on this Meteor/mobservable affair. Any advice??

Thanks.

bySabi commented 8 years ago

it is 'half' working, Tracker.autorun(function() { ... is not retriggered on findOne changes. Any idea??

bySabi commented 8 years ago

Well. mobservable observe changes on 'afiliado' reference but I need it observe on afiliado object fields. My ignorance make me treat afiliado like a plain Object and isnot. I have to conclude this is not way?? Any advice are welcome ...

mweststrate commented 8 years ago

Hi @bySabi

Happy new year as well! I'm not a meteor expert, so I'm wondering, does the findOne in autorun return the same instance every time or a new object? If the same object is returned each time it will probably suffice to make the affiliado object observable itself so that changes to it are picked up, assuming that they are plain objects further themselves (sorry, last time with meteor is a few years ago, I should pick it up again I guess ;-)). Probably affiliado = observable(...findOne...) will do the trick in that case.

An issue of your current solution might be that your Tracker.autorun subscription is never disposed. (should be done on componentWillUnmount).

I think your example can be simplified to:

var afiliado = mobservable.observable(); // observable reference
var autorunner = Tracker.autorun(function() {
      // HARDCODE MongoID for test only
      afiliado(Orgz.collections.Afiliados.findOne({_id: 'EbPM2uWJhbd8rZP8P'}));
});

ViewPersona = mobservableReact.observer(React.createClass({
  render() {
    return <ViewPersona_ afiliado={afiliado()} />;
  },
  componentWillUnmount() {
     autorunner.stop();
  }
}));

ViewPersona_ = React.createClass({
  render() {
    return <AfiliadoTabs afiliado={this.props.afiliado} />;
  }
});

I think it would be really nice if the Tracker.autorun could be skipped, I'm gonna tinker about whether that is possible :)

edit: use setter

bySabi commented 8 years ago

Hello @mweststrate

I don´t know, yet, is findOne return the same reference each time. If we make this code work sure would know it.

I try your suggestions but fail with: Uncaught Error: [mobservable.observable] Please provide at least one argument.

Thanks for the componenWillUnmount part this will next step to do is first step get solved.

On meteor community we are a little confused on what to choose. Many developers migrate from TFRP based architecture of Blaze templates to React but is not clear what architecture choose cause Flux & sons overlap with Meteor client-side cache/livequery/... I don´t really like Redux on Meteor, is a wonderful solution, well done and easy. But after a half year with Tracker TFRP I feel Flux it is a downgrade. I'm a Solo developer, a one man shop, that need all the magic behind that I can have. I´m sure that you and mobservable will really welcome on Meteor community but before that we need show some working example to involved them. Is this work my next step is move https://github.com/meteor/simple-todos-react to mobservable.

I can make a repo with all is needed for test Meteor and mobservable if you wanna jump to the wagon.

mweststrate commented 8 years ago

Sorry, listing should start with observable(null). A test repo for meteor and mobservable would be nice indeed!

On Sat, Jan 2, 2016 at 12:16 PM, bySabi notifications@github.com wrote:

Hello @mweststrate https://github.com/mweststrate

I don´t know, yet, is findOne return the same reference each time. If we make this code work sure would know it.

I try your suggestions but fail with: Uncaught Error: [mobservable.observable] Please provide at least one argument.

Thanks for the componenWillUnmount part this will next step to do is first step get solved.

On meteor community we are a little confused on what to choose. Many developers migrate from TFRP based architecture of Blaze templates to React but is not clear what architecture choose cause Flux & sons overlap with Meteor client-side cache/livequery/... I don´t really like Redux on Meteor, is a wonderful solution, well done and easy. But after a half year with Tracker TFRP I feel Flux it is a downgrade. I'm a Solo developer, a one man shop, that need all the magic behind that I can have. I´m sure that you and mobservable will really welcome on Meteor community but before that we need show some working example to involved them. Is this work my next step is move https://github.com/meteor/simple-todos-react to mobservable.

I can make a repo with all is needed for test Meteor and mobservable if you wanna jump to the wagon.

— Reply to this email directly or view it on GitHub https://github.com/mweststrate/mobservable/issues/84#issuecomment-168381378 .

bySabi commented 8 years ago

Good! .. it is working! reactively on findOne changes. :-) but ... it throw a exception on changes:

dnode.js:207 [mobservable.view '<component>#.1.render()'] There was an uncaught error during the computation of ComputedObservable[<component>#.1.render() (current value:'undefined')] function reactiveRender() { 

I will do a test repo in a couple of hours.

I stay on touch, this promise ...

mweststrate commented 8 years ago

I think after that exception another exception is logged with the actual error that did occur

bySabi commented 8 years ago

Only have one, the above.

mweststrate commented 8 years ago

hmm then the exception is eaten somewhere, the above warning comes from a finally clause. I'll await the test repo :)

bySabi commented 8 years ago

Hi @mweststrate here is the repo: https://github.com/bySabi/simple-todos-react-mobservable

I try migrate to mobservable without luck. The original is update a little. mobservable stuff is on: client/App.jx

I you wanna test original implementation just rename: client/App.jsx.ReactMeteorData to client/App.jsx

luisherranz commented 8 years ago

@bySabi there you go man: https://github.com/bySabi/simple-todos-react-mobservable/pull/1

I have tried to modify your example as less as possible.

mweststrate commented 8 years ago

@bySabi sorry, didn't find the time yet to dive into this, @luisherranz nice, tnx a lot! A generic patterns starts to appear already in your PR :)

Something like:

const state = observable({
   tasks: []
})
syncWithMeteor(state, 'tasks', () => Tasks.find({}, {sort: {createdAt: -1}}).fetch())

function syncWithMeteor(obj, attr, func) {
   tracker.autorun(() => {
      mobservable.extendObservable(obj, { [attr]: func() })
   });
}
luisherranz commented 8 years ago

I don't know if you had time to read the endless conversation with James (sorry! :sweat_smile:), you don't need to if you don't want to, don't worry.

The thing is I have fallen in love with Mobservable. It's vastly superior to Tracker and I am going to make an adapter so people coming from Meteor (who already know what TRP and its benefits but have switched from Blaze to React due to the latest Meteor changes) can swap Tracker with Mobservable.

I have some ideas on how to make the integration. An obvious one could be a wrapper like the one in your last comment, but there are more options.

I understand how Tracker works really well but I have some questions about Mobservable. I'd love to have a conversation about the internals of Mobservable, to see if that idea is feasible. In Tracker, the dependencies (Tracker.Dependency) are disconnected from the actual data, so Tracker doesn't know about the data, per se. Therefore, I am not sure if that approach would work.

If you have 5 minutes this week for this let me know and we can meet in your discord channel :+1:

bySabi commented 8 years ago

Great! idea! .. reimplement Tracker API with mobservable. If you need a beta tester ....

@mweststrate I think that me and the JS community in general need a new article from you. Right now everybody, me include, is obssesed with purity, inmutability, side-effects paranoia and that´s why flux pattern buy us on first place. But I see a little burocracy on this pattern, say a component can´t take any decision. It must ask someone(emit a 'action') cause we cannot trust it for mutate data, even if the data is simple like a boolean var. We need a pattern based on transparent reactivity than enable us mutate data in a predictive way without needed for thirty party trusted reducers. Maybe you can write another Medium article explaining your thought about and put a example mobservable vs redux. And stop a little with the inmutability madness. I´m sure many people on JS community glad you.

dcworldwide commented 8 years ago

@luisherranz glad to know i'm not alone in this kind of thinking. Like sabi said, happy to review your work if and when you get to it.

luisherranz commented 8 years ago

Thanks @dcworldwide!

@mweststrate I want to start with this but I'm having a kind of a hard time getting the feature/2 mobservable branch to load on meteor.

If you have any other idea, or you can fix the build errors, or maybe push a beta version to npm let me know :)

bySabi commented 8 years ago

@luisherranz you can install local packages this way. Clone 'mobservable' on 'app-1.3-test/packages` and install it:

npm install packages/mobservable --save

and this work too:

npm install https://github.com/mweststrate/mobservable.git#feature/2 --save
luisherranz commented 8 years ago

Uhm...

This throws an 406 error on npm install

"dependencies": {
  "mobservable": "https://github.com/mweststrate/mobservable/tree/feature/2"
}

And the tarball installs fine but doesn't work

"mobservable": "https://github.com/mweststrate/mobservable/tarball/feature/2"

because the libs folder is missing.

If I clone the repo on packages and run npm install packages/mobservable --save I get the same typescript compiler errors.

mweststrate commented 8 years ago

Yeah I still have to expose the atom api from the package in the 2 branch. Is it ok that I come back to you early next week? Otherwise my family will rightfully complain about not celebrating a weekend off ;)

luisherranz commented 8 years ago

No problem my friend :)

ghost commented 8 years ago

Any updates on replacing tracker with mobx?

mweststrate commented 8 years ago

Nope, I think it should be quite doable. But I would need to learn meteor first. I think it should ideally be done by someone with a lot of Meteor knowledge and supported by MDG.

Nonetheless I think it is still possible to combine MobX and Tracker, for example by wrapping stuff in MobX autorun that notifies a Tracker Dependency when needed.

mquandalle commented 8 years ago

Hi, I’m working on a Meteor application (Wekan) that I would like to convert to React. After studying the different possibilities for data handling, I see a lot of value in Mobx as a “better Tracker, for React”.

The transition path from Tracker to React for most reactive data structures is relatively straightforward (eg replacing Session variables by Mobx observables is trivial) however one abstraction I miss is the client side reactive Minimongo cursors. Minimongo cursors are useful if you want to display a reactive subset of a collection, which is exactly what we do in Wekan (eg display all the cards with a specific tag).

So I’m super interested in an implementation of the Tracker API on top of Mobx. I took a look at it (early draft repository here: https://github.com/mquandalle/tracker-mobx-bridge) and at least some of the abstractions seems to match reasonably well (a Tracker dependency ~= Mobx atom for instance). I don’t have a good enough understanding of the Mobx internals to decide the best equivalences between every element of the Tracker API but I’m confident that the building a bridge is doable.

bySabi commented 8 years ago

@mquandalle I try to archive this goal too, mobTracker but stop due time constrains and lack of knowledge. You can take the name is you like it. :-)

Your intentions are good but maybe not worth the effort spent on this task cause incoming Apollo project. In my IMO, MDG probably deprecate Tracker and Minimongo.

Maybe a better effort is bring mobX to Apollo, evangelize a little :-)

mquandalle commented 8 years ago

Yes, having the client side reactive cache of Apollo based on MobX is an attractive idea. We sure should evangelize that!

On the meantime I realize that the only high level feature that I’m missing from a MobX based “store” is the possibility to have cursor on a set of data. What is cool with a Cards.find({ author: 'authorId' }) cursor is that the observer is only recalled when a card that we care about is inserted, modified, or deleted. I guess I could just use a _.filter on a MobX array but that’s neither as efficient nor elegant. Are you aware of any good way to solve this problem @mweststrate? Is there somethink like a “Collection” build on top of MobX?

mweststrate commented 8 years ago

@mquandalle I have to dive deeper in to this, but a short question: Are you primarily interested on cursors that tracks remote collections, or local connections (a filter seems to suggest the latter?). For these kind of advanced patterns atoms for lifecycle management and transformers for smart map/reduce like patterns might be key.

Edit: Wekan looks cool :)

mquandalle commented 8 years ago

I was thinking about local collections (that turns out to be synced with the server, but the syncing mechanism should be considered as an independent part that doesn’t interfere with it).

So if I have on my client collection a list of todo items:

let todoItems = observable([
  { id: "1", title: "abc", completed: false },
  { id: "2", title: "def", completed: true },
  { id: "3", title: "efg", completed: false },
]);

and I want to keep track reactively of the items that are not completed yet (items 1 and 3 in the above example) to display them in a view. What is the canonical way to do so? The good thing with a cursor is that it creates an atom that gets invalidated iff a document matching its selector gets modified.

mweststrate commented 8 years ago

in that case I would go for todoItems.filter(todo => !todo.completed). I wouldn't worry to much about performance as it will only observe mutations to the array itself and all the relevant completed attributes. But if your collections are really large, you can improve this by utilizing a transformer that memoizes the completed state:

const completedChecker = createTransformer(todo => !todo.completed);
const completedItems = todoItems.filter(completedChecker);
mweststrate commented 8 years ago

Closing this one for inactivity.

DominikGuzei commented 8 years ago

Just to let you know, we are using Meteor with redux and mobx and this is the code that glues Meteor.Tracker and mobx autorun together:

import { autorun as mobxAutorun } from 'mobx';
import { Tracker } from 'meteor/tracker';

export default (reaction) => {
  let mobxDisposer = null;
  let computation = null;
  let hasBeenStarted = false;
  return {
    start() {
      let isFirstRun = true;
      computation = Tracker.autorun(() => {
        if (mobxDisposer) {
          mobxDisposer();
          isFirstRun = true;
        }
        mobxDisposer = mobxAutorun(() => {
          if (isFirstRun) {
            reaction();
          } else {
            computation.invalidate();
          }
          isFirstRun = false;
        });
      });
      hasBeenStarted = true;
    },
    stop() {
      if (hasBeenStarted) {
        computation.stop();
        mobxDisposer();
      }
    }
  };
};

We call this reaction and you can pass in any function that depends on reactive data sources in the Meteor world and/or mobx observables like this:

reaction(myFunctionThatShouldAutorun).start();

We tried a lot of different combinations, this was the only way we could achieve a reliable invalidation of both systems.

mweststrate commented 8 years ago

That looks pretty smart! Thanks for sharing. cc: @faceyspacey

DominikGuzei commented 8 years ago

Thanks @mweststrate, we really love mobx by the way! This solved all our issues with Meteor & redux, and reduced the boilerplate mess about 80%. Anyone reading this and still using things like Tracker.Component should immediately stop and rethink.

mweststrate commented 8 years ago

Here is a nice blog on how Meteor and MobX can be combined: http://markshust.com/2016/06/02/creating-multi-page-form-using-mobx-meteor-react

timeyr commented 8 years ago

I am also trying to use MobX within Meteor. The main (only?) issue I am facing is mapping the data sent from the server (in the form of documents in a (Mini)Mongo collection) to observable objects and making sure that the correct instance of each observable is re-used.

My solution is to create a store for each Mongo collection and use Mongo.Cursor.observe() to insert/update/delete the items in the store based on what happens in the collection. However things get compicated when your domain models contains heavily de-normalized relational data and you need to keep track of all the references. Another compilcation is the need to for composite subscriptions in order to have access to all referenced data. This is why my gut feeling is that Mongo collections are not appropriate for relational data. My medium-term goal would be to use Apollo (i.e. GraphQL) because that makes querying for relational data so much easier.

DominikGuzei commented 8 years ago

@timeyr in most cases you can design your store properties around real application use-cases which often do not map 1-to-1 to collection data. For example the mobx based store might mix some collection data and client-side only state properties.

That's why we setup reactions for those parts that need a reactive dependency on collection data. Then you don't have to care about adding / removing specific documents with observeChanges etc.

MongoDB is definitely not made for relational data and you always have to completely denormalize for the UI. One way that makes this easy, is to use EventSourcing where you can generate any number of projections (= denormalized view of your relational data), even after the-fact, so it's future proof (when you don't know exactly what data you will need to show in 1 year == pretty much any project).

timeyr commented 8 years ago

@DominikGuzei, can you show me an example of what you mean by "setting up reactions on collection data"?

DominikGuzei commented 8 years ago

@timeyr did you see the code i pasted a few comments above? You have to put this into a file like /imports/client/lib/reaction.js and then you can write reactions like this:

import store from '/imports/client/store';
import { Meteor } from 'meteor/meteor';

export default () => {
  const state = store.getState();
  const isLoggingIn = Meteor.loggingIn();
  // This assumes you have a "route" mobx object mapped as reducer
  const isAuthRequired = state.route.isAuthRequired;

  // Update the auth mobx state based on Meteor user presence
  state.auth.isLoggedIn = Meteor.user() ? true : false;

  // Anonymous user landed on a restricted page -> login is required
  if (!state.auth.isLoggedIn && !isLoggingIn && isAuthRequired) {
    store.dispatch({ type: 'LOGIN_REQUIRED' });
  }
};

This is just a basic function that works with Meteor collections or any reactive data source and also with mobx objects. In itself it would not do anything special, nor be reactive etc. Now you just have to turn it into a reaction with the help of reaction code above:

import reaction from '/imports/client/lib/reaction';
import authReaction from '/imports/client/reactions/auth';
reaction(authReaction).start();

Now your reaction autoruns whenever a Meteor or Mobx dependency invalidates – in this example whenever:

Usage with redux

We create a reducer that always returns the same mobx instance as state:

import { assign } from 'lodash';
import { observable } from 'mobx';

const auth = observable({
  isLoggedIn: false,
  loginError: null
});

export default (state, {data, type}) => {
  switch (type) {
    case 'LOGIN_ERROR':
      return assign(auth, { loginError: data });
    default: return auth;
  }
};

and then you register this as normal reducer on redux store:

import { combineReducers, createStore } from 'redux';
import auth from '/imports/client/reducers/auth.js';
export default store = createStore(combineReducers({ auth }));

Voilá, now you have redux + mobx + Meteor setup that is fully reactive and supports all directions of updates. So you can update the mobx object properties in reducers or in reactions.

DominikGuzei commented 8 years ago

As a bonus, here is a fictional reaction that deals with "relational" data and multiple documents:

import store from '/imports/client/store';
import { Meteor } from 'meteor/meteor';
import { Projects } from '/imports/client/collections';

export default () => {
  const state = store.getState();
  const userId = state.auth.userId;

  if (state.auth.isLoggedIn) {
    Meteor.subscribe('user-projects', userId);
    state.projects.my = Projects.find({ ownerId: userId }).fetch();
    state.projects.shared = Projects.find({ collaborators: { $in: [userId] }}).fetch();
  }
};
timeyr commented 8 years ago

@DominikGuzei: Suppose now that you want to display one of these Projects in some view. Because you probably need to store some additional UI state on them, you'll wrap them in an observable, together with references to other (observable) domain objects. But now you need to implement an identity map for those objects while keeping them in sync with your server data/collections. This is what I am struggling with. Of course this is not a problem with MobX, but rather more general I suppose.

DominikGuzei commented 8 years ago

@timeyr sorry i cannot follow you – this is fully reactive, so you can calculate / map / identify / relate anything you want within reactions. The only thing you have to be careful with, is that you don't introduce too many reactive dependencies, as this can result in too many re-runs. But from what i hear in your response is that you would need a dedicated projection (= denormalized collection) that delivers exactly the data structure optimized for this particular view. This is what EventSourcing brings to the table – based on your event history you can generate any kind of structural and temporal projections, no matter how complex the relations are (there is simply no limitation). I would even say: if you need to display your domain data in many different ways and relations (and also temporal correlations, like "Which products did the customer remove from the shopping cart 2 min before checkout") there is hardly a way around event sourcing.

markshust commented 8 years ago

I love @DominikGuzei's reactor -- great stuff. This is great for non-react items.

Note that you can do something similar for react containers using the new composeWithMobx function of react-komposer, if all you want to do is sync unified mobx & tracker autorun data down to react presentation components https://github.com/kadirahq/react-komposer/#using-with-mobx

example:

    export default composeAll(
      composeWithMobx(onPropsChange),
      composeWithTracker(onPropsChange),
      useDeps(depsMapper),
    )(ComponentName);

I really like @DominikGuzei's code for non-react state changes though or just setting up general reactions/listeners.

DominikGuzei commented 8 years ago

@markoshust thanks for the update! we will publish a space:reaction package + docs soon that provides the functionality described above, so you don't have to copy the code over and read through this thread 😉

markshust commented 8 years ago

great, i was hoping you would! :)

jmaguirrei commented 8 years ago

Hi, I followed @DominikGuzei reaction pattern and have success defining integrated stores with Meteor backend.

This is the architecture:

/imports/stores/lib/reaction.js

import { autorun as mobxAutorun } from 'mobx';
import { Tracker } from 'meteor/tracker';

export default (reaction) => {
  let mobxDisposer = null;
  let computation = null;
  let hasBeenStarted = false;
  return {
    start() {
      let isFirstRun = true;
      computation = Tracker.autorun(() => {
        if (mobxDisposer) {
          mobxDisposer();
          isFirstRun = true;
        }
        mobxDisposer = mobxAutorun(() => {
          if (isFirstRun) {
            reaction();
          } else {
            computation.invalidate();
          }
          isFirstRun = false;
        });
      });
      hasBeenStarted = true;
    },
    stop() {
      if (hasBeenStarted) {
        computation.stop();
        mobxDisposer();
      }
    }
  };
};

The Store synced with Meteor.users Collection: /imports/store/Timefic/00_App/(( Users )).js

import {
  mobx,
  Constants,
  SubsManager,
} from '/lib/imports/client.js';

import { default as reaction } from '/imports/stores/lib/reaction.js';

import { myConsole } from '/imports/dev/functions/myConsole.js';
import { State } from '/imports/stores/State/state.js';
import { myPeople } from './functions.js';
import { profileStyle } from '../01_Office/functions.js';

/*
***************************************************************************************************

  U S E R S    S T O R E

***************************************************************************************************
*/

const
  { COMPANY_ID } = Constants.DEFAULTS,
  fromMeteor = {
    myUser: {},
    myContacts: [],
    allPeople: [],
    officeProfile: {},
    ready: false,
  },
  usersSub = new SubsManager();

function getMeteorData() {

  usersSub.subscribe('users', State['App.company_id']);

  const
    // If Company is defined by user it's in App State
    myUser_id = Meteor.userId(),

    // Office Profile
    cursorX = State['Office.cursorX'],
    cursorY = State['Office.cursorY'],
    openProfileId = State['Office.openProfileId'];

  Tracker.autorun((thisComp) => {
    // console.log('Tracker ' , Tracker);
    if (myUser_id && usersSub.ready() && !State['App.company_id']) {
      State.modify({
        'App.company_id': Meteor.users.findOne(myUser_id).base.defaults.company_id
      }, 'APP_STORE_COMPANY_ID_READY');
      thisComp.stop();
    }
  });

  if (usersSub.ready() && State['App.company_id']) {

    return {

      // People
      get myUser() {
        return myPeople({
          Users: Meteor.users.find({ _id: myUser_id }).fetch(),
          company_id: State['App.company_id'],
        })[0];
      },

      // myContacts / Colleagues:
      // If company is Timefic --> Contacts, if not --> Colleagues

      get myContacts() {

        if (State['App.company_id'] === COMPANY_ID) {

          // myContacts
          return myPeople({
            Users: Meteor.users.find(
              { 'base.contacts': { $in: this.myUser.base.contacts }},
            ).fetch(),
            company_id: State['App.company_id'],
          });

        }
        // myColleagues
        return myPeople({
          Users: Meteor.users.find({
            _id: { $ne: myUser_id },
            [`base.company.${State['App.company_id']}`]: { $exists: true }
          }).fetch(),
          company_id: State['App.company_id'],
        });

      },

      // myPeople --> myContacts && Me
      get allPeople() {
        if (this.myUser && this.myContacts) {
          return _.concat([ this.myUser ], this.myContacts);
        }
        return false;
      },

      // Open Profiles
      get officeProfile() {
        if (this.allPeople && openProfileId) {
          const
            profile = _.find(this.allPeople, { _id: openProfileId }),
            style = profileStyle({ cursorX, cursorY, openProfileId });
          return _.assign(profile, { style });
        }
      },

      get ready() {
        return this.myContacts &&
          this.myUser &&
          this.allPeople && true;
      },
    };
  }

  return fromMeteor;

}

class Store {

  constructor() {

    mobx.extendObservable(this, _.assign({
      // Fields not synced with Meteor
    }, fromMeteor));

    reaction(() => {
      if (getMeteorData().ready) {
        mobx.extendObservable(this, _.assign({}, getMeteorData()));
      }
    }).start();

    // Provide Console
    this.console = () => myConsole(this, 'USERS_STORE');
  }

}

const UsersStore = new Store();

export { UsersStore };

A file with a React Container and Presentational Component: /client/modules/Timefic/01_Office/03_User.jsx


import {
  React,
  MyImage,
  observer,
} from '/lib/imports/client.js';

import { UsersStore } from '/imports/stores/Timefic/00_App/(( Users )).js';
import { OfficeActions } from '/imports/stores/Timefic/01_Office/actions.js';

/*
***************************************************************************************************

  O F F I C E   U S E R

  Description here

***************************************************************************************************
*/

const styles = (coords, zColor) => ({

  'office-user-item': {
    /* Structure */
    position: 'absolute', top: `${coords.y}px`, left: `${coords.x}px`,
    /* Skin */
    borderRadius: '50%', boxShadow: `0 0 2px 4px ${zColor}, 0 0 2px 4px black`,
  },

  'office-user-item-dot': {
    /* Structure */
    position: 'absolute', top: '25px', left: '25px',
  },

});

const _User_ = ({
    actions: { onToggleUser },
    data: { allPeople },
  }) => {

  return (

    <div id='office-user'>
      {
        allPeople.map(people => {
          const
            { zColor } = people,
            { coords } = people.zOffice;

          return (
            <div key={people._id} style={styles(coords, zColor)['office-user-item']}>
              <MyImage
                _id={`user-${people._id}`} _class='office-pic'
                src={people.zActivePic}
                skin='ROUNDED/SHADOW'
                size='M'
                onClick={(event) => onToggleUser(event, people._id)}
              />
            </div>

          );
        })
      }
    </div>
  );
};

const User = () => {

  const
    actions = _.pick(OfficeActions, [ 'onToggleUser' ]),
    data = _.pick(UsersStore, [ 'allPeople' ]),
    ready = UsersStore.ready;

  if (ready) {
    return <_User_ actions={actions} data={data} />;
  }
  return false;

};

export default observer(User);

I have several stores and components and I tested that reactions worked OK even with nested attributes, for example fields 'base.name' or 'base.defaults.company_id' on Meteor users collection.

The thing is I am having wasted time for my React Perf analysis, so I am wondering if the Store should be defined with createTransformer or computed properties, instead of the way they are now ...

@mweststrate @markoshust @DominikGuzei I am having calls to render components that doesnt touch the DOM so I am wondering if there are some better Mobx way to define ths Store for this case.

Thanks!

markshust commented 8 years ago

I really am trying to stay away from intermingling Meteor & MobX, to stay with the single source of truth principle. The fact that Meteor data sources are already reactive, means that an integration of MobX for data sources is really pointless imo, and there could be situations where the data eventually store gets out of sync with the state store, is hard to debug, etc.

I'm having GREAT results using react-komposer, and instead of passing in both composeWithMobx and composeWithTracker calls directly to composeAll, I'm separating each out into onPropsChange and onDataChange functions (respectively), allowing each composer to handle their respective source. This is leading to highly readable code. I'm using the container built with composeWithTracker as a higher level component that just passes props to the container built with composeWithMobx. See here for an example of usage for handling fetching data from Meteor, piping it to MobX for a data recordset edit form: https://gist.github.com/markoshust/92d5368c52de480c087bf47e81e9318b -- I think this is a good proper use of syncing Meteor data with MobX, but notice it's not reactive, I'm just using it to set defaultValue's of form elements for editing the record.

See https://github.com/kadirahq/react-komposer/pull/99 for your re-render issues. This uses react-komposer and your code doesn't, however it's possible something is related here for you.

jmaguirrei commented 8 years ago

@markoshust If I understand correctly you are separating the Data (coming from Meteor Mongo backend) from Stores (coming from Mobx observables) and feeding both into your Container Component, and then to the component.

Although this seems clear, my point is:

1) With this approach React Component are not observers anymore, because Mobx are not tracking whats happens in Meteor Collections, right? 2) The Mobx Store just holds "pure" UI State, as React State does, for example: isThisWindowOpen, selectedConversation, currentCompany, etc...

For what I understand if 1) and 2) are like I say, then what's the point of Mobx? If Mobx doesn't know about data, then the components doesn't have to observe (fine-grained) changes.

It's more like a traditional React + State approach, dont?

For what I see in this article by @mweststrate the Domain Stores should hold your data to allow Mobx to control (very efficiently) the render events of your UI:

https://mobxjs.github.io/mobx/best/store.html

I am not saying your approach isn't good, I just want to understand why "without Mobx" (if I am correct you are almost not using Mobx) you can get better performance, just the render calls the UI need. Does this compose Mantra modules does this job, this "ShouldComponentUpdate" under the hood?

markshust commented 8 years ago

@jmaguirrei I'm using mobx for ui state -- it is definitely used (a lot), and a core component of my application architecture. Basically, anywhere I would use React's setState, I'm using MobX instead.

If you think about it, you don't pipe the Meteor datasets into React's setState. I really don't think any MobX performance enhancements are worth the hassle unless you have an extremely large dataset you are working with (1,000+ items to render on screen, or advanced/complex ui state).

jmaguirrei commented 8 years ago

@markoshust Let me make my point by example:

Lets say you have a: 1) Meteor Mongo Collection called 'Conversations', with 4 fields:

2) Mobx State called 'selectedConversation' (used to filter the collection after the user selects one conversation)

3) A ConversationDetail component that shows fields i & ii of the selected Conversation.

4) A ConversationStats component that shows fields iii & iv of the selected Conversation.

Scenario A: A new message arrives, so the fields iii & iv change. Scenario B: A new member is added to the conversation, so field i change.

If you are composing with Tracker and feeding into components, how do they now:

markshust commented 8 years ago

This is the situation that needs https://github.com/kadirahq/react-komposer/pull/99

Theoretical shouldResubscribe usage:

Scenario A

const differentDoc = (currentProps, nextProps, currentContext, nextContext) =>
  currentProps.members !== nextProps.members
    || currentProps.createdAt !== nextProps.createdAt;
const Clock = compose(onPropsChange, null, null, {shouldResubscribe: differentDoc})(Time);

Scenario B

const differentDoc = (currentProps, nextProps, currentContext, nextContext) =>
  currentProps.lastMessage.text !== nextProps.lastMessage.text
    || currentProps.messageCount !== nextProps.messageCount;
const Clock = compose(onPropsChange, null, null, {shouldResubscribe: differentDoc})(Time);