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!!!
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.
it is 'half' working, Tracker.autorun(function() { ... is not retriggered on findOne changes.
Any idea??
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 ...
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_
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.
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 [email protected] 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
.
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 ...
I think after that exception another exception is logged with the actual error that did occur
Only have one, the above.
hmm then the exception is eaten somewhere, the above warning comes from a finally clause. I'll await the test repo :)
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
@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.
@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() })
});
}
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.
Posts.find()) it returns an Astronomy class instead of a cursor, so I could hook in the same he is doing and return a mobservable observable instead. If you have 5 minutes this week for this let me know and we can meet in your discord channel :+1:
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.
@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.
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.
reportObserved and reportAtomChanged. If you have any other idea, or you can fix the build errors, or maybe push a beta version to npm let me know :)
@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
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.
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 ;)
No problem my friend :)
Any updates on replacing tracker with mobx?
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.
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.
@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 :-)
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?
@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 :)
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.
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);
Closing this one for inactivity.
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.
That looks pretty smart! Thanks for sharing. cc: @faceyspacey
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.
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
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.
@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).
@DominikGuzei, can you show me an example of what you mean by "setting up reactions on collection data"?
@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:
isAuthRequired required property changes (mobx)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.
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();
}
};
@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.
@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.
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.
@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 š
great, i was hoping you would! :)
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!
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.
@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?
@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).
@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:
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);
Then, in a large app, you should manually implement _shouldResuscribe_ on every component that uses a portion of a Mongo record (I think almost every component in the app)...
@markoshust I think that's the whole point of using observables at all!
Not worring about fine-grained dependencies. Not doing manually things that a piece software can do.
See
https://mobxjs.github.io/mobx/refguide/observer-component.html
Characteristics of observer components
- Observer only subscribe to the data structures that were actively used during the last render. This means that you cannot under-subscribe or over-subscribe. You can even use data in your rendering that will only be available at later moment in time. This is ideal for asynchronously loading data.
- You are not required to declare what data a component will use. Instead, dependencies are determined at runtime and tracked in a very fine-grained manner.
- Usually reactive components have no or little state, as it is often more convenient to encapsulate (view) state in objects that are shared with other component. But you are still free to use state.
- @observer implements shouldComponentUpdate in the same way as PureRenderMixin so that children are not re-rendered unnecessary.
- Reactive components sideways load data; parent components won't re-render unnecessarily even when child components will.
- @observer does not depend on React's context system.
That's the point also to make Meteor Data observable ("load it into a Mobx Store") so the Store will react to changes in Mongo, but the observers of the Store will only react if their "contracts" says to....
@mweststrate @DominikGuzei do you agree?
I get what you are saying... but it really depends on your use-case :) I see situations where you can use both approaches. I could be a bit side-tracked right now, as I'm building a CMS with editable forms, so I specifically "don't" want Meteor to react with updates, as the forms are editable.
That said, it should be fairly simple to pipe the data into mobx and then use that as the single source of truth. See https://github.com/mobxjs/mobx/issues/84#issuecomment-223747251 for example :)
@jmaguirrei you made the important observation here. Mobx is all about giving developers their dream back: simple state management where you don't need a master thesis to understand how it is composed by 1 million different functions. At the same time, ultra-fast and fine grained updates without any manual work as you already said. If you use Mobx in strict mode you even get the last and important feature of redux: immutable state that can only be updated by Mobx actions. For me redux is dead, the only (sad) reason im still using it in some places is because of the great work on libraries like redux-form.
By the way: we released the Mobx / Tracker integration code i posted above as a meteor package ready to use in your projects: https://github.com/meteor-space/tracker-mobx-autorun
Please give us feedback on the documentation etc. š
@markoshust You are right, it depends on your case.
In my case my app needs to be fully reactive and was getting really slow because of my lack of knowledge to implement shoulComponentUpdate. I was (or I think I was) following all best practices in React, almost all stateless components and one component for the whole state. But a month ago I get to the point I cant type in an input box without noticing a really annoing delay. Every component was 'tied' to the app component in a way, so the whole was rendered again and again.
That's why I refactor everything, and adopted Mobx principles.
@DominikGuzei I will definitely check it out!
I am also going to be using strict mode (didn't heard about it until today), if this is the best way to get the benefits from Mobx.
The thing maybe you could help me to ask is:
Is it true that if I define correctly my Mobx Domain Store (for example Conversations synced with Meteor backend), who takes some data from other Stores (the UI StateStore or another Domain Store), computes some fields, and returns an object with the relevant data to the UI (nothing more nothing less than the data that is logical part of this domain), denormalized but not duplicated, the Observer Components will allways re-render only if the relevant portion of the store has changed?
If the answer is Yes, what can I be doing wrong in the example code I posted today? Can you check that and tell me if you see something that should be better implemented?
Thanks Dominik. Hello everyone, tracker-mobx-autorun is solution that is tested on a relatively big client project. Reduction in boilerplate code is amazing. We started out using Tracker.Component but we quickly replaced it with this as we saw it as an anti-pattern. MobX is really awesome and performant. Currently we have hybrid Redux/MobX solution on that project, since almost all forms are redux-form based.
We have aslo ported mantra-sample-blog to this ui stack to showcase usage of this library on a real project. We will make that public this week.
@darko-mijic Yes, I gave it a re-read now and I am wondering 2 things:
1) Its clear I will always use computed values that are based on observables "primitive" values. I think I was achieving this using extendObservable on get object properties, for example:
// Open Profiles
get officeProfile() {
if (this.allPeople && openProfileId) {
const
profile = _.find(this.allPeople, { _id: openProfileId }),
style = profileStyle({ cursorX, cursorY, openProfileId });
return _.assign(profile, { style });
}
},
Now I dont know if I this field is computed or not, how can I check that? Or should I return
return computed(_.assign(profile, { style }))
instead?
2) I only have observer on the container component because the "dumb" component does nothing with data, only pass the data to the JSX markup (please see my code above). Should I still be including observer on dumb components?
@jmaguirrei I think I was randomly seeing lags in inputs as well, it must be do to react-komposer. I think I'll need to rethink a few things of my decided implementation to move to more idiomatic mobx... you are all convincing me this is probably the way to go.
@markoshust Check if that input is deeply nested in the component hierarchy and/or it mutates state that have an impact on the top level component.
I think the component should be as decoupled as possible. One thing is to be in a deep level in the tree, that's inevitable, but another is to be "deep and tight" (props => props => props).
In the Mobx architecture, almost all my components are parameterless functions:
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);
As you see, the component itself talks to the store(s) and pick the data they need and renders its dumb component (_User_).
I am still not 100% sure that's the best (performant and mantainable) way to go, but make sense and it will make 100% sense when I see React Perf Wasted Time = 0
@jmaguirrei pretty sure the issue is I'm not using mobx idiotmatically. this composeWithMobx container composer works great, however like those who pointed this out before me, it's not taking advantage of mobx-react's observer capabilities and diff matching. I think I may still use composeWithMobx in a few places, however it's causing extra, unnecessary renders because it appears to re-render on every state change, because it doesn't use the same logic that is in mobx-react. like those who said before, the whole reason i'm using mobx is to ditch the extra work (such as the resubscribe/shouldComponentUpdate stuff), so I see no reason not to switch this to idiomatic mobx and just use the suggested tools for this architecture.
@markoshust What do you mean when you say "idiomatically"? Can you give me an example?
@darko-mijic Cant wait to see this!
We have aslo ported mantra-sample-blog to this ui stack to showcase usage of this library on a real project. We will make that public this week.
@darko-mijic can't wait to see this too! please post up here once that is ready.
@jmaguirrei just meaning using the official/suggested tools "the mobx way". packages such as react-komposer are great, but mobx is magic, and should use magic implementations to take full advantage of it's tech.
FYI there appears to be some significant interest in meteor + mobx (+ mantra), and it's been a super long time since I've done any screencasts. So there's something on the way as soon as I can carve out a few ;)
Looking forward to that @markoshust !
No worries, I will post a link here @markoshust and @jamiewinder. It would be nice to discuss it and hear feedback. My port will be Mantra-free, Mantra is boilerplate generator in my opinion.
Here is an example Store with https://github.com/meteor-space/tracker-mobx-autorun and the more idiomatic Mobx I was able to done!:
Though I am still having unnecesary re-renders, still looking to get the best use of Mobx observables
import {
mobx,
Messages,
SubsManager,
autorun,
} from '/lib/imports/client.js';
import { myConsole } from '/imports/dev/functions/myConsole.js';
import { State } from '/imports/state/state.js';
import { myMessages } from './functions/myMessages.js';
const { observable, computed, action, useStrict } = mobx;
useStrict(true);
/*
***************************************************************************************************
M E S S A G E S S T O R E
***************************************************************************************************
*/
class Store {
@observable myMessages;
@computed get isInConversationReady() {
return this.myMessages.length > 0 && State['Chat_isInConversation'];
}
}
const
consoleActive = true,
MessagesStore = new Store(),
messagesSub = new SubsManager();
/* ------------------------------------------------------------------------------------------------
myMessages
------------------------------------------------------------------------------------------------ */
autorun(() => {
messagesSub.subscribe('messages', { conversation_id: State['Chat_conversation_id'] });
action('MESSAGES_STORE: myMessages', (messages) => {
MessagesStore.myMessages = _.filter(
myMessages({
Messages: messages,
people_ids: State['Chat_conversationPeople'],
selectedTab: State['Chat_selectedTab'],
myUser_id: Meteor.userId(),
}),
item => item.agendaNum === State['Chat_selectedAgendaNum'] || State['Chat_selectedTab'] !== 3
);
myConsole(MessagesStore, 'MESSAGES_STORE', 'green', consoleActive);
})(
State['Chat_conversation_id'] ?
Messages.find({ conversation_id: State['Chat_conversation_id'] }, { sort: { date: 1 }}).fetch() :
[]
);
}).start();
export { MessagesStore };
@jmaguirrei thanks, should the subscribe be within the autorun?
@markoshust I had the same wondering and according to this example, it should be inside:
https://github.com/meteor-space/tracker-mobx-autorun
I think if it's outside will not react to state changes.
If subscription does not depend on state, it could be outside.
Also because I am using Subsmanager, I think there is no over subscription
@markoshust Also see this, it's a pattern to get not "over reactivity", I will be refactoring today according to this and monitoring results with React Perf.
@jmaguirrei I think what you are experienced was a concern of mine, please see the last comment at https://github.com/meteor-space/tracker-mobx-autorun/issues/5#issuecomment-236375354
I'm worried that over-subscribing to data will cause a lot of listeners that never get cleaned up. When a component mounts, the autorun should be started, and when it unmounts, the autorun should be stopped.
I think there should be start/stop functionality on mounting/unmounting... no? The subscribe should be in the start function, not within the autorun. Technically, on any state change, it is creating a new subscriber (whether it's a performance issue or not is another question, perhaps not, but I think technically it should be outside of the autorun). I'm also using SubsManager but this should be setup in a manner which works with or without.
I think you should really be destructuring your store object like so in your contianer:
const { a, b } = store;
<List a={a} />
<List2 b={b} />
instead of passing the entire store to your presentational components. You should be setting up your presentation components to not reference Store, so they just work off props and be used if store is used or not.
@markoshust In the "old" way of getting data from Meteor with the react-meteor-data package (https://atmospherejs.com/meteor/react-meteor-data) the subscription is also inside the function. I think your concerns are valid, but maybe Meteor handle this? (just guessing...)
In respect to passing the store to components, I will only use that pattern for reusable components.
The problem with:
class MyContainer extends React.Component {
render() {
const { observableA, observableB } = store;
return (
<div>
<List a={observableA} />
<List2 b={observableB} />
</div>
);
}
}
Is that if observableA change then MyContainer, List and List2 are re-rendered. Same thing if observableB changes.
On the other hand, if you just pass the store or (better) dont pass anything but let the components to import the data they need, no "sided" renders will take effect.
Hmm, that doesn't seem correct. If observableA is passed in, it should just render List. That is the whole idea of the observer function. Let me test things out. and I'll report back.
You're right about react-meteor-data -- it is inside the function. I guess it's not a problem because of that. I still think we need a way to stop the autoruns/subscriptions on a cleanup/componentWillUnmount function.
Until yesterday I was convinced that just List would be rendered but the thing that @andykog pointed out in https://github.com/mobxjs/mobx-react/issues/94, is that with this pattern the Parent (MyContainer) observes A & B, so if A changes it will re-render, and if he re-render then all it's child are re-rendered too.
Today I changed my components to import what they need and I am finally getting 0 wasted time.
Only pass not observable data as props.
Another thing that it's important to be aware is that you should also "granularize" your observables, because when they are invalidated it generates reactions in all observables (naturally), so if they are grouping things, they will cause unnecesary re-renders also.
So, rule of thumb:
He's wrong -- I put up a dummy repo to reproduce this. Feel free to clone it out and test:
https://github.com/markoshust/mobx-render-test
Yep, sorry for that. For some reason, I was sure, that forceUpdate results in skipping shouldComponentUpdate on children as well. Shame on me.
@markoshust You are right... I was getting unnecesary re-rendering because I wasn't using observer() in Item1 & Item2 child components... I was using it before, but I remove them because "I wanted not to re-render so better don observe here...." The thing is totally in the other way. If you observe in all your components, then you avoid unnecesary re-renders.
If you remove them from your example, you will see that they always re-renders.
So, I am optimist again, just observe "everything" and there should be no problem.
Thanks.
@jmaguirrei, you can apply pureRenderMixin, or something like https://www.npmjs.com/package/pure-render-decorator instead of making observer. This happens just because @observer already implement pureRenderMixin for you
@andykog If I understand correctly, there are 2 ways then:
Is that correct? If so, whats the benefits of the first way over the second?
whats the benefits of the first way over the second
@jmaguirrei, the benefit is that child component can be ādumbā. It can know nothing about your stores and depend only on props.
This article is targeting redux, but I think the concept is suitable for mobx too https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0#.b5oufte81
@jmaguirrei yep, i think the mobx approach is "observer everwhere" and you won't have any issues. it also won't have performance problems on components not using mobx.
@andykog it appears mobx-react's observer does a bit more -- there is some mobx code in there? https://github.com/mobxjs/mobx-react/blob/master/src/observer.js#L146 -- I'm not sure, I'm completely stupid about pureRenderMixin and the different approaches :)
yep, i think the mobx approach is "observer everwhere" and you won't have any issues. it also won't have performance problems on components not using mobx.
I like making UI components completely not aware of stores. That way they can be reused later, even in non-mobx app. So I wouldn't say āobserver everwhereā is always a good idea.
it appears mobx-react's observer does a bit more -- there is some mobx code in there
that 3 lines are just checking if update is scheduled. If yes, no need to update ringt now, because it will happen anyway in a moment. The pureRendering implementation is the rest of the method.
@andykog @markoshust For some reason at React doesnt use PureRender by default. So if we decorate all dumb components with this maybe can loose reactivity.
For now I am confident about importing stores (to container components) instead of passing props, because I feel more control over the data flow.
We'll see. I am developing a large app, so I am sure I will encounter some more "rocks in the road".
@andykog thanks for the implementation info. I was wondering how to rid my code of observer calls all over the place. I'll test this out.
For some reason at React doesnt use PureRender by default
@jmaguirrei, React can't use PureRender by default becouse then it will fail when one of the props is some complex tree that was changed deeply inside. Then it needs to make deep equality check, resulting in problems with performance. Or some of the props may not be a plain object, but some class that can't be compared at all. So when you implemen PureRender by yourself you are taking the responsibility not to make such sneaky things.
So if we decorate all dumb components with this maybe can loose reactivity
Why? We won't loose reactivity for sure :-D
thanks for the implementation info. I was wondering how to rid my code of observer calls all over the place. I'll test this out.
@markoshust, you can simply replace it with pure render decorator, but I doubt that you'll notice some considerable difference in performance.
@markoshust and @jamiewinder, here is the initial draft of ported Mantra Sample Blog:
https://github.com/darko-mijic/mantra-sample-blog-app/pull/1
There is only one thing I miss from Redux and that is redux-form.
@darko-mijic - FYI, I think you're intending to reference someone other than me? :)
Sorry about that ;)
Slightly off topic, but may I ask you how you have your stores subscribe to Meteor collections? Do you fetch all data beforehand, i.e. all documents in the collection with all fields? Because your store is unaware of the concrete data requirement of the react components.
Data can be filtered on publication level by specifying document fields that will be published. You can also transform and map data fetched from publications in autoruns.
In most of my project i am using event sourcing which gives me the ability to generate fully denormalized collections based on view requirements using projections. That makes things much easier. The only source of truth in event sourced system is the commit store which holds immutable stream of domain events. Projections are than used to generate fully denormalized view cache. Here is an example of one simple projection:
https://github.com/meteor-space/projection-rebuilding-example/blob/feature/ui/source/imports/application/projections/transactions-projection.js
@darko-mijic @timeyr I am having more reactions than needed, I think maybe you already dealed with that.
I have a collection named Meetings in Meteor (Mongo) with this structure:
// In the server
// Collection
const Meetings = new Mongo.Collection('meetings');
// Schema
const MeetingsSchema = new SimpleSchema({
_id: {type: String},
title: {type: String},
date: {type: Date},
tags: {type: Object},
'tags.main': {type: [ String ]},
'tags.related': {type: [ String ]},
people: {type: Object},
'people.owner': {type: [ String ]},
'people.members': {type: [ String ]},
'people.watchers': {type: [ String ]},
agenda: {type: [ Object ], optional: true},
'agenda.$.title': {type: String},
'agenda.$.status': {type: String},
'agenda.$.time': {type: Object, optional: true},
'agenda.$.time.total': {type: Number},
'agenda.$.time.remaining': {type: Number},
'agenda.$.content': {type: Object, optional: true, blackbox: true},
agreements: {type: [ Object ], optional: true},
'agreements.$.content': {type: Object, optional: true, blackbox: true},
'agreements.$.likes': {type: [ String ]},
'agreements.$.dislikes': {type: [ String ]},
commitments: {type: [ Object ], optional: true},
'commitments.$.people': {type: [ String ]}, // 0: responsible, 1: revisor
'commitments.$.content': {type: Object, optional: true, blackbox: true},
'commitments.$.dueDate': {type: Date},
comments: {type: [ Object ], optional: true},
'comments.$.people_id': {type: String},
'comments.$.content': {type: Object, optional: true, blackbox: true},
status: {type: String},
company_id: {type: String, index: 1},
});
Meetings.attachSchema(MeetingsSchema);
Meteor.publish('meeting', function ({ _id }) {
return Meetings.find({ _id });
});
I am subscribing to the whole document only when user had selected 1 meeting_id.
So I am using autorun from https://github.com/meteor-space/tracker-mobx-autorun to observe changes and update the Store:
// Meeting Store
class Store {
// @observable mymeetings;
@observable myMeeting_core;
@observable myMeeting_people;
@observable myMeeting_agenda = [];
@observable myMeeting_agreements = [];
@observable myMeeting_commitments = [];
@observable myMeeting_comments = [];
}
const
consoleActive = true,
MeetingStore = new Store(),
meetingSub = new SubsManager();
/* ------------------------------------------------------------------------------------------------
myMeeting
------------------------------------------------------------------------------------------------ */
autorun(() => {
meetingSub.subscribe('meeting', { _id: State['Meeting_meeting_id'] });
action('MEETINGS_STORE: myMeeting', () => {
if (meetingSub.ready() && State['Meeting_meeting_id']) {
const myMeetingInfo = myMeeting({
meeting: Meetings.find({ _id: State['Meeting_meeting_id']}).fetch()[0],
allPeople: CoreStore.allPeople,
});
MeetingStore.myMeeting_core = _.pick(
myMeetingInfo, [ '_id', 'title', 'date', 'tags', 'status' ]
);
MeetingStore.myMeeting_people = _.pick(
myMeetingInfo, [ 'zOwner', 'zMembers', 'zWatchers', 'zCommentators', 'zCommitters' ]
);
MeetingStore.myMeeting_agenda = myMeetingInfo.agenda;
MeetingStore.myMeeting_agreements = myMeetingInfo.agreements;
MeetingStore.myMeeting_commitments = myMeetingInfo.zCommitments;
MeetingStore.myMeeting_comments.replace(myMeetingInfo.zComments);
}
myConsole(MeetingStore, 'MEETINGS_STORE', 'pink', consoleActive);
})();
}).start()
Each field is an array of objects and has its correspondent component (container):
The thing is either Agenda, Agreements, Commitments or Comments has a change (I use a terminal to update a document directly in Mongo), the whole Store is reacting, for example:
Change content in agreement number (2) => Agenda, Agreements, Commitments, Comments are re-rendered... That's not OK.
// Comments.jsx
const Comments = () => {
const { myMeeting_core, myMeeting_comments } = MeetingStore;
if (myMeeting_core && myMeeting_comments) {
const
actions = {
onAddMeetingMember: MeetingActions.onAddMeetingMember,
onDelMeetingMember: MeetingActions.onDelMeetingMember,
},
data = {
meeting_id: myMeeting_core._id,
zComments: myMeeting_comments,
};
console.log('Comments Render');
return <_Comments_ actions={actions} data={data} />;
}
return false;
};
export default observer(Comments);
I think if I split the document into separate collections, I can solve that, but it looks wrong to force that.
So, what am I doing wrong?
Should I use different collections in Mongo to achieve that?
Or use different projections for each field in the store, remaining same collection?
@jmaguirrei: I will take a look at your post tomorrow.
BTW, have you seen new API with live query support?
https://github.com/meteor-space/tracker-mobx-autorun/pull/10
@darko-mijic Yes, I knew about it today hoping to help me with my issue.
If you think this could help in my case, I can give it a try tomorrow.
Still I'll appreciate your feedback first about my code :)
Thanks
@darko-mijic I think the thing is that in this piece of code:
autorun(() => {
meetingSub.subscribe('meeting', { _id: State['Meeting_meeting_id'] });
action('MEETINGS_STORE: myMeeting', () => {
if (meetingSub.ready() && State['Meeting_meeting_id']) {
const myMeetingInfo = myMeeting({
meeting: Meetings.find({ _id: State['Meeting_meeting_id']}).fetch()[0],
allPeople: CoreStore.allPeople,
});
MeetingStore.myMeeting_core = _.pick(
myMeetingInfo, [ '_id', 'title', 'date', 'tags', 'status' ]
);
MeetingStore.myMeeting_people = _.pick(
myMeetingInfo, [ 'zOwner', 'zMembers', 'zWatchers', 'zCommentators', 'zCommitters' ]
);
MeetingStore.myMeeting_agenda = myMeetingInfo.agenda;
MeetingStore.myMeeting_agreements = myMeetingInfo.agreements;
MeetingStore.myMeeting_commitments = myMeetingInfo.zCommitments;
MeetingStore.myMeeting_comments.replace(myMeetingInfo.zComments);
}
myConsole(MeetingStore, 'MEETINGS_STORE', 'pink', consoleActive);
})();
}).start()
The variable myMeetingInfo changes after any part of the document in Mongo changes (for example a content in agreement number 2), so autorun fires the mobx action that updates all observable values in the store:
But, in practice observables 1) 3) 4) has not changed.
So I think 2 ways to solve this:
1) Mobx detects that new value equals old value, so not observer (mobx-react) has nothing to deal.
2) I have to split the code or the collection to have independent observers. This is much boilerplate, but doable.
So, thinking about option 1) there is already a way to achieve this? Do you agree with the approach?
Most helpful comment
Thanks Dominik. Hello everyone,
tracker-mobx-autorunis solution that is tested on a relatively big client project. Reduction in boilerplate code is amazing. We started out using Tracker.Component but we quickly replaced it with this as we saw it as an anti-pattern. MobX is really awesome and performant. Currently we have hybrid Redux/MobX solution on that project, since almost all forms are redux-form based.We have aslo ported mantra-sample-blog to this ui stack to showcase usage of this library on a real project. We will make that public this week.