hi, I have a question:
The state of my application contains arrays of objects, mobx apparently does not track changes on the fields of these objects neither changes on the arrays.
I went through the documentation, asked a related question on SO, and checked the issues 214 and 374 but still it's not clear to me how i should proceed.
I think that #214 and #374 are unrelated to your problem.
Maybe you somewhere introduce a new property without making the property observable?
Probably at:
this.props.myState['blocks'][this.props.index][event.target.name] = event.target.value
Property [event.target.name] of "block" object is not observable, it's changes won't be tracked.
Notice:
const state = observable({ a: null });
// state is an object with observable property "a"
// if "a" is accessed in render method, the component will be re-rendered when "a" changes
state.b = [];
// Introducing new property will NOT make the property observable
// if "b" is accessed in render method, the component will NOT be re-rendered when "b" changes
If you want to add observable properties dynamically, you have two options:
1) Don't use object properties, use ObservableMap:
const state = mobx.map({ a: null })
state.set("b", [])
state.get("b")
=> this.props.myState['blocks'][this.props.index].set(event.target.name, event.target.value)
2) Use mobx.extendObservable(state, { b: [] })
=> mobx.extendObservable(this.props.myState['blocks'][this.props.index], { [event.target.name]: event.target.value })
Hi.
I also had this kind of question on: SO
I've realized that I have to either keep telling Mobx about my data in rather explicit fashion or make a workaround using a sentinel or proxy data that I directly manipulate. In my case the problem is perhaps harder because I can't (well, I shouldn't) peek inside the data I'm presenting.
It may be that Mobx is not a good fit for my case. Nevertheless I'd like to entertain the possibility of
I don't yet have sufficient experience with Mobx to give advice on your question, but options above seem correct to me. Since objects seem hard to observe, using maps or extending observable should work (but that's probably a different data model than you currently have).
@jussiarpalahti about that proxy ... I've been thinking how I would implement it, so I came up with this:
const Mobx = require("mobx");
class ObservableActiveObj {
constructor({ activeObj: activeObj }) {
this._activeObj = activeObj;
// Make observable
Mobx.extendObservable(this, {
// Define state representing a possible change of activeObj internal state
this._changed = Date.now(); // (value can be anything unique)
// Turn needed properties exposed by activeObj into computed getters
get selectedValue() {
const unused = this._changed; // make sure it will recompute when this._changed changes
return this._activeObject.selectedValue; // notice it won't re-render unless the returned value really changes
}
})
// Turn methods into actions
this.change = Mobx.action(this.change.bind(this));
}
// Provide needed activeObj methods
change(...args) {
// Invalidate
this._changed = Date.now();
// Delegate call
return this._activeObj.change(...args);
}
}
One could probably automated it to some extend:
Def: function observableProxy(nonObservableObj, observableProps, actions)
Usage: observableProxy(active_obj, ["selectedValue"], ["change"])
Maybe it could be fully automated by iterating through all property names (including nonenum/inherited), checking for prop descriptors/types and converting setters/functions to actions and getters/others to computed... not sure, there might be some issues...
If the the original object is more complex and there is some nesting, we could return the proxied nested object in computed getter:
get nestedObj() {
const unused = this._changed;
return new ObservableNestedObj({ nestedObj: this._activeObject.nestedObject });
}
What do you think? Are there any problems with this implementation? Do you have a better solution?
EDIT: In case there is no "reading" API (no "selectedValue" prop etc) exposed by library class instance, then the whole state is owned by you and the internal state of the instance is nothing more than a derivation of your state.
When you call actionObj.change(args) the args is a state owned by you. All your components should react to args change, not to actionObj change.
actionObj.change(args) should be invoked as a reaction to args change. I hope the logic is right...
Thanks for the suggestion, @urugator
This might work for observator, but there's the observer's part to this. React component's render method needs to make use of the changed value in order for Mobx see it needs to react (meaning: you have to access it). If I don't use the observed, changed value (here obj._changed) anywhere I don't think Mobx will see that as a signal for observer to react. At least this has been my experience, but I haven't used extendObservable myself so it might alter this behavior.
I'll have to test this approach. Though I fear this might easily turn into Mobx within Mobx ;)
@jussiarpalahti
React component's render method needs to make use of the changed value in order for Mobx see it needs to react (meaning: you have to access it). If I don't use the observed, changed value (here obj._changed) anywhere
In render you simply access proxied selectedValue prop. The subscription to _changed is done in it's getter. However thanks to computed value being cached, it will re-render only when _activeObject.selectedValue changes as mentioned in comments. If you would subscribe directly for obj._changed it would always re-render with a change of obj._changed (which is not desired).
extendObservable
The extendObservable is used only because I don't use decorators...
@urugator I tried the solution you provided with the unused timestamp object and it works!
This solution seems quite hacky though. I just wonder what should be the proper way to handle these scenarios.
@riccardolorenzon
Well you have to invalidate the react component somehow. There are two mechanisms provided:
1) setState() (can be replaced by Mobx)
Problem is that setState() can only be invoked as a reaction to something.
Since activeObj doesn't provide a way to react to it's changes (by producing events or by being observable), you can't really use setState without introducing own state, representing the possible change of activeObj state.
It doesn't have to be a timestamp, it can be something meaningful (like itemSelected, nameChanged, etc) which led to invocation of activeObj.change method - possibly as a Mobx.reaction. But your component(s) will need to know how your state is tangled with activeObj state.
2) Passing new props
This will always work, but you have to pass the (already dereferenced) property to every component which depends on it. So anytime you need to access activeObj.something you have to pass activeObj.something directly as a prop.
The problem here is that you still need to somehow force the root component (holding the activeObj itself) to re-render to pass updated activeObj.something to it's children.
Which will ultimately bring you back to option 1)
Notice that non of it will work if activeObj.change is async.
EDIT: I just wanted to show that using Mobx isn't the cause of the problem itself.
@urugator I understand how the rendering for a react component works, the issue i have is that since i moved to Mobx the components that pick their value from a nested object within the state are not re rendered anymore after a change on that value.
The state is passed as a props to the sub components from the main component(and this would be the scenario # 2 in your description), but apparently the re rendering does not happen.
In order to get it i had to add a timestamp property as a first level property in the state, and update it in the sub components causing in that way the re rendering of the parent and of the sub components.
But this seems quite a hacky way to proceed.
I haven't yet have time to test the implementation on my project yet, but this has been my experience in general. Since Mobx (very helpfully) decides for you (if you don't use setstate I guess) when component should re-render, component's render method won't be called if Mobx doesn't see something being used that it knows has changed.
Or in pseudo-React:
render () {
const dummy_value = this.props.proxy_obj.invalidating_dummy_value;
return <some-sub-component arg=this.props.proxy_obj />
}
Other way to make this work is make observers out of every component in the tree using proxy object, but that's not possible in my case since I only use library's top level component.
Another way to deal with this is to use toJS to convert the value before passing it to the non-observer component.
I solved it by creating different classes(ES6 classes) for each nested object. I could do it because the properties set is limited.
In that way the rendering works as expected without any hacks.
@riccardolorenzon would you mind giving an example of what you did? Did you just decorate an ES6 class with @ observable and use instances of that class to fill your array?
@dsandor In my case the array contained only instances of the same class.
I then modeled this class as a ES6 class and decorate each field of this class as @observable.
To fill the array i used instances of that class. All the changes to any observable field were tracked properly then.
Is this not working for you?
@riccardolorenzon yes, thanks. This did in fact work for me.
@riccardolorenzon This worked for me as well! Thanks a lot.
Most helpful comment
I think that #214 and #374 are unrelated to your problem.
Maybe you somewhere introduce a new property without making the property observable?
Probably at:
this.props.myState['blocks'][this.props.index][event.target.name] = event.target.valueProperty
[event.target.name]of "block" object is not observable, it's changes won't be tracked.Notice:
If you want to add observable properties dynamically, you have two options:
1) Don't use object properties, use ObservableMap:
const state = mobx.map({ a: null })state.set("b", [])state.get("b")=>
this.props.myState['blocks'][this.props.index].set(event.target.name, event.target.value)2) Use
mobx.extendObservable(state, { b: [] })=>
mobx.extendObservable(this.props.myState['blocks'][this.props.index], { [event.target.name]: event.target.value })