extending of observable subclassing: [MobX] Options can't be provided for already observable objects - mobx

When I create a deep structure with few extends I got this kind of error:
Uncaught Error: [MobX] Options can't be provided for already observable objects.
"mobx": "^6.4.2",
"mobx-react-lite": "^3.3.0",
All of code is just dirty example. real structure more complex.
Code example:




import { makeObservable, observable, action } from 'mobx';
class DateMobx {
date ;
constructor(data) {
this.date = new Date(data.date);
makeObservable(this, { date: observable }, { autoBind: true });
}
}
class Todo extends DateMobx {
id = 0;
title = 'title';
text = 'text';
constructor(todo) {
super(todo);
this.id = todo.id;
this.title = todo.title;
makeObservable(this, { id: observable, title: observable, changeTitle: action }, { autoBind: true });
}
changeTitle= (e) => {
const { value } = e.target;
this.title = value;
}
}
class TodoList {
todo = [];
constructor() {
const todoData = [{ id: 1, title: 'title', date: '123' }];
this.todo = todoData.map(item => new Todo(item)); // ERROR happen here
makeObservable(this, { todo: observable }, { autoBind: true });
}
}


Error happen in constructor of TodoList class.

if remove makeObservable from Todo class, error is not reproduced but I need reactivity in that class.

If remove extends DateMobx from Todo class error also is not reproduces (but I have a lot of general classes with basic logic where I need reactivity too).



Why its happen and what should I do if I really need such kind of deep structure ?

Guys on github bring me the right solution
Don't pass the third param ({ autoBind: true }) to makeObservable in subclasses. All options are "inherited" and can't be changed by subsequent makeObservable calls on the same object (this).
options argument can be provided only once. Passed options are
"sticky" and can NOT be changed later (eg. in subclass).
https://mobx.js.org/observable-state.html#limitations

Related

Vue3: how to pass an object from provide to index

I've been using vue.js for a few weeks and I would like to understand how to globally inject to child components an object coming from the server.
When I try to inject the object using inject:['user'] to a child component it returns an empty object.
data() {
return {
user: []
}
},
methods: {
getLoggedUserData() {
axios.get('/api/get-user/' + window.auth.id
).then(response => {
this.user = response.data.user;
});
}
},
provide: {
return {
user: this.user
}
},
created() {
this.getLoggedUserData();
}
The provide option should be a function in this case to get access to this.user property:
export default {
provide() {
return {
user: this.user
}
}
}
For descendants to observe any changes to the provided user, the parent must only update user by subproperty assignment (e.g., this.user.foo = true) or by Object.assign() (e.g., Object.assign(this.user, newUserObject)):
export default {
methods: {
async getLoggedUserData() {
const { data: user } = await axios.get('https://jsonplaceholder.typicode.com/users/1')
// ❌ Don't do direct assignment, which would overwrite the provided `user` reference that descendants currently have a hold of:
//this.user = user
Object.assign(this.user, user) ✅
}
}
}
demo

Vue | define reactive property in plugin

Trying to build my own form validation plugin (it's for learning purposes - so I don't use existing libraries).
So I created the following mixin:
export default {
beforeCreate() {
if (! this.$vnode || /^(keep-alive|transition|transition-group)$/.test(this.$vnode.tag)) {
return;
}
// create
this.$validator = new Instance();
// define computed
if (! this.$options.computed) {
this.$options.computed = {};
}
this.$options.computed['errors'] = function() {
return this.$validator.errors;
};
}
}
And loaded the mixin from the component (cause I don't want to see this anywhere):
export default {
name: "SignIn",
components: {
AppLayout,
TextField,
HelperText,
Button
},
mixins: [ValidateMixin]
}
Anyway, anytime input has changed - there is an event which tests the value and controls my errors bag:
export default class {
constructor() {
this.items = {};
}
first(name) {
if (name in this.items) {
return this.items[name][0];
}
return false;
}
add(name, errors) {
this.items[name] = errors;
}
remove(name) {
delete this.items[name];
}
has(name) {
return name in this.items;
}
all() {
return this.items;
}
}
I've bind HTML element (:invalid="errors.has('email')"), and with the devtools I can see the errors bag changing - but the binding is just doesn't work. The invalid property remains false no matter what I'm doing.
I do understand that in order to create reactive property, I've to handle this with getters/setters, but I'm a bit stuck with it.

Mobx: reaction for observing object doesn't work

Here is my store:
import { observable, action, flow, reaction } from "mobx";
export default class Demo {
#observable obj = {
flag: false,
name: "",
age: 20
};
#action
turnFlag = () => {
this.obj.flag = true;
};
constructor() {
reaction(
() => this.obj,
obj => {
console.log(obj.flag);
}
);
}
}
What I want do is, if any property in obj changed, the reaction callback will be invoked.
But when the action turnFlag executed, nothing happened.
So what's wrong with my code? If I want supervisor any change in obj, what should I do?
To make the reaction work you need to have it watch a property on the observable, rather than the root obj.
reaction(
() => this.obj.flag,
flag => { console.log(`FOO: ${flag}`); }
);
There's a working example of that here: https://codesandbox.io/s/km3n38yrj7
(Open your browser console to see the output.)
The documentation covers this here:
It is important to notice that the side effect will only react to data that was accessed in the data expression, which might be less then the data that is actually used in the effect.
In your original code you weren't accessing anything on 'obj'.
Since you want to do something when anything on 'obj' is changed:
What I want do is, if any property in obj changed, the reaction callback will be invoked.
It sounds like you instead want 'observe'.
observe(this.obj, change => {
console.log(
`${change.type} ${change.name} from ${change.oldValue}` +
` to ${change.object[change.name]}`
);
});
I've updated the codesandbox link to show that.
let's try so as
`import { observable, action, flow, reaction } from "mobx";
class Demo {
#observable obj = {
flag: false,
name: "",
age: 20
};
#action
turnFlag = () => {
this.obj.flag = true;
};
constructor() {
reaction(
() => this.obj,
obj => {
console.log(obj.flag);
}
);
}
}
export default Demo;
`

An element descriptor's .kind property must be either "method" or "field"

I'm following the documentation for mobx-react-router but upon attempting to run my application I get the following error in the browser:
Uncaught TypeError: An element descriptor's .kind property must be either "method" or "field", but a decorator created an element descriptor with .kind "undefined"
at _toElementDescriptor (app.js:49988)
at _toElementFinisherExtras (app.js:49990)
at _decorateElement (app.js:49980)
at app.js:49976
at Array.forEach (<anonymous>)
at _decorateClass (app.js:49976)
at _decorate (app.js:49958)
at Module../src/App/UserStore.js (app.js:50012)
at __webpack_require__ (bootstrap:19)
at Module../src/index.js (index.js:1)
Here is how I intitialize:
const appContainer = document.getElementById('app');
if(appContainer) {
const browserHistory = createBrowserHistory()
const routingStore = new RouterStore();
const stores = {
users: userStore,
routing: routingStore
}
const history = syncHistoryWithStore(browserHistory, routingStore);
ReactDOM.render(
(
<Provider {...stores}>
<Router history={history}>
< App />
</Router>
</Provider>
),
appContainer);
}
And this is how I use:
#inject('routing')
#inject('users')
#observer
class App extends Component { ...
My UserStore:
import { observable, action, computed } from "mobx"
class UserStore {
#observable users = [];
#action addUser = (user) => {
this.users.push(user)
}
#computed get userCount () {
return this.users.length
}
}
const store = new UserStore();
export default store;
I've tried to Google for this error but it's returning no useful results. Any ideas what I am doing wrong?
If you're using Babel 7 install support for decorators:
npm i -D\
#babel/plugin-proposal-class-properties\
#babel/plugin-proposal-decorators
Then enable it in your .babelrc or webpack.config.js file:
{
"plugins": [
["#babel/plugin-proposal-decorators", { "legacy": true}],
["#babel/plugin-proposal-class-properties", { "loose": true}]
]
}
Note that the legacy mode is important (as is putting the decorators proposal first). Non-legacy mode is WIP.
Reference: https://mobx.js.org/best/decorators.html
While the accepted answer works, for anyone coming here from the same error message but a different context, this is likely because the decorator's signature changed as the proposal progressed.
Using { "legacy": true } uses one method signature, while {"decoratorsBeforeExport": true } uses another. The two flags are incompatible.
You can inspect what's happening with the given example
function log() {
console.log(arguments)
}
class Foo
#log
bar() {
console.log('hello')
}
}
new Foo().bar()
Using {"decoratorsBeforeExport": true } will yield
[Arguments] {
'0': Object [Descriptor] {
kind: 'method',
key: 'bar',
placement: 'prototype',
descriptor: {
value: [Function: bar],
writable: true,
configurable: true,
enumerable: false
}
}
}
Where {"legacy": true } would give you
[Arguments] {
'0': Foo {},
'1': 'bar',
'2': {
value: [Function: bar],
writable: true,
enumerable: false,
configurable: true
}
}
By not using the legacy semantics, writing a decorator is fairly simple. A decorator which returns its 0th argument is a no-op. You can also mutate before returning. Using the new semantics, you can describe a decorator as follows:
function log(obj) {
console.log('I am called once, when the decorator is set')
let fn = obj.descriptor.value
obj.descriptor.value = function() {
console.log('before invoke')
fn()
console.log('after invoke')
}
return obj
}
I changed the observable like so and it works (although not sure why):
import { observable, action, computed } from "mobx"
class UserStore {
#observable users;
constructor() {
this.users = []
}
#action addUser = (user) => {
this.users.push(user)
}
}
const store = new UserStore();
export default store;

Onsen + VueJS: Call back from child component (using onsNavigatorProps)

Per documentation here
If page A pushes page B, it can send a function as a prop or data that modifies page A’s context. This way, whenever we want to send anything to page A from page B, the latter just needs to call the function and pass some arguments:
// Page A
this.$emit('push-page', {
extends: pageB,
onsNavigatorProps: {
passDataBack(data) {
this.dataFromPageB = data;
}
}
});
I am following this idea. Doing something similar with this.$store.commit
I want to push AddItemPage and get the returned value copied to this.items
//Parent.vue
pushAddItemPage() {
this.$store.commit('navigator/push', {
extends: AddItemPage,
data() {
return {
toolbarInfo: {
backLabel: this.$t('Page'),
title: this.$t('Add Item')
}
}
},
onsNavigatorProps: {
passDataBack(data) {
this.items = data.splice() //***this*** is undefined here
}
}
})
},
//AddItemPage.vue
...
submitChanges()
{
this.$attrs.passDataBack(this, ['abc', 'xyz']) // passDataBack() is called, no issues.
},
...
Only problem is this is not available inside callback function.
So i can't do this.items = data.splice()
current context is available with arrow operator.
Correct version:
onsNavigatorProps: {
passDataBack: (data) => {
this.items = data.splice()
}
}