Sebastian Olsen Sebastian Olsen - 2 months ago 15
React JSX Question

MobX not setting observable properly in my class

I'm trying to make my react app as dry as possible, for common things like consuming a rest api, I've created classes that act as stores with predefined actions to make it easy to modify it.

Behold, big code:

import {autorun, action, observable} from 'mobx'

export function getResourceMethods(name) {
let lname = name.toLowerCase()
let obj = {
methods: {
plural: (lname + 's'),
add: ('add' + name),
addPlural: ('add' + name + 's'),
rename: ('rename' + name),
},
refMethods: {
add: ('add' + name + 'ByRef'),
addPlural: ('add' + name + 'sByRef'),
rename: ('rename' + name + 'ByRef'),
setRef: ('set' + name + 'Ref'),
},
fetchMethods: {
pending: (lname + 'Pending'),
fulfilled: (lname + 'Fulfilled'),
rejected: (lname + 'Rejected'),
}
}
return obj
}

class ResourceItem {
@observable data;
@observable fetched = false;
@observable stats = 'pending';
@observable error = null;

constructor(data) {
this.data = data;
}
}

class ResourceList {
@observable items = [];
@observable fetched = false;
@observable status = 'pending';

constructor(name) {
this['add' + name + 's'] = action((items) => {
items.forEach((item, iterator) => {
this.items.push(item.id)
})
})
}
}

class ResourceStore {
constructor(name, resourceItem, middleware) {
let {methods} = getResourceMethods(name)

this.middleware = middleware || []

let items = methods.plural.toLowerCase()
this[items] = observable({}) // <--------------- THIS DOES NOT WORK!

// Add resource item
this[methods.add] = action((id, resource) => {
let item = this[items][id], data;

if (item && item.fetched) {
data = item.data
} else {
data = resource || {}
}

this[items][id] = new resourceItem(data)
this.runMiddleware(this[items][id])
})

// Add several resource items
this[methods.addPlural] = action((resources) => {
resources.forEach((resource, iterator) => {
this[methods.add](resource.id, resource)
})
})

// Rename resource item
this[methods.rename] = action((oldId, newId) => {
let item = this[items][oldId]
this[items][newId] = item

if (oldId !== newId) {
delete this[items][oldId]
}
})
// Constructor ends here
}

runMiddleware(item) {
let result = item;
this.middleware.map(fn => {
result = fn(item)
})

return result
}
}

class ReferencedResourceStore extends ResourceStore {
@observable references = {}
constructor(name, resource, middleware) {
super(name, resource, middleware)

let {methods, refMethods, fetchMethods} = getResourceMethods(name)

let getReference = (reference) => {
return this.references[reference] || reference
}

this[refMethods.setRef] = action((ref, id) => {
this.references[ref] = id
})

this[refMethods.add] = action((ref, data) => {
this[methods.add](getReference(ref), data)
this[refMethods.setRef](ref, getReference(ref))
})

this[refMethods.rename] = action((ref, id) => {
this[methods.rename](getReference(ref), id)
this[refMethods.setRef](ref, id)
})

// *** Fetch *** //

// Resource pending

this[fetchMethods.pending] = action((ref) => {
this[refMethods.add](ref)
})

// Resource fulfilled

this[fetchMethods.fulfilled] = action((ref, data) => {
this[refMethods.add](ref, data)
this[refMethods.rename](ref, data.id)

let item = this[methods.plural][data.id];
item.fetched = true
item.status = 'fulfilled'
})

}
}

export {ResourceItem, ResourceList, ResourceStore, ReferencedResourceStore}


Now I'm just creating a simple user store:

class UserResource extends ResourceItem {
constructor(data) {
super(data)
}

@observable posts = new ResourceList('Posts')
@observable comments = new ResourceList('Comment')
}

// Create store
class UserStore extends ReferencedResourceStore {}
let store = new UserStore('User', UserResource)


And
mobx-react
connects just fine to the store, can read it as well. BUT, whenever I do any changes to the items (
users
in this case, the name of the property is dynamic) property, there are no reactions. I also noticed that in chrome, the object property does not have a "invoke property getter" in the tree view:

enter image description here

Answer

Didn't read the entire gist, but if you want to declare a new observable property on an existing object, use extendObservable, observable creates just a boxed observable, so you have an observable value now, but not yet an observable property. In other words:

this[items] = observable({}) // <--------------- THIS DOES NOT WORK!

should be:

extendObservable(this, {
  [items] : {}
})

N.b. if you can't use the above ES6 syntax, it desugars to:

const newProps = {}
newProps[items] = {}
extendObservable(this, newProps)

to grok this: https://mobxjs.github.io/mobx/best/react.html

Edit: oops misread, you already did that, it is not hacky but the correct solution, just make sure the extend is done before the property is ever used :)