WARNING: Wild idea, RFC.
Currently, things like reference are kinda hacky and work only on models (no array or maps).
At the moment in MST we have 3 kind of types:
I was wondering, why not make them all complex?
Each complex type consists already of methods to get/set its value and get/apply it's snapshot.
We could use shallowBox to create a container for basic types, and simply set/get over it. The result of create() over basic types would be a basic shallowBox observable. Instead, accessing it from a model property would return it's containing value. (is this an acceptable thing?). Basically each property would be a getter/setter.
import {types as t} from 'mobx-state-tree'
const a = t.number.create(1) // => a is shallowBox<number>()
const Todo = t.model({ id: t.number })
const b = Todo.create({id: 2}) // => b.id is number
Ideally, if weakmaps were a thing, we could store adm nodes in a weakmap
We can now ensure that EVERY MST instance, regardless of it's type, has a MST Administration Node.
This will make implementing reference easy. References will now be a type instead of being a weird thing over models. Their get and set will resolve the referenced object, and their get/apply snapshot will set/get the {$ref} object.
The only tricky part will be arrays, basically, we'll need to keep stored the array of childNodes.map(node => node.get()). e.g. an array of reference should get() the instances, but getSnapshot() the array of ref objects. Wondering if this way we can keep advantage of observe/computeds in MST arrays.
Also, when we will implement #76, we can now implement them on types, and not only on models.
const NikiMinaj = withLifecycle(
t.string,
{
postprocessSnapshot: snapshot =>({ILikeBigButtsAndICannotLie: snapshot})
})
What do you think @mweststrate ?
Futher investigations:
We can introduce a new type in the IType definition, which is the Box type for immutable values.
Basically, when create() ing, instead of returning the type T, we can return a "Box". A box, is an object that will contain the value changing over time. While the value can change over time, the box will remain the same. This will use under the hood the IObservableValue from mobx.
IType will look like IType, some examples:
t.maybe(t.number).create() // => IObservableValue<number | null>
t.maybe(t.array(t.number)).create() // => IObservableValue<IObservableArray<number> | null>
t.number.create() // => IObservableValue<number>
To do so, It would be necessary to create a "NodeAdministration" for each box.
In collection types like arrays and objects, the object/array itself will also be the Box type. (because a reference of them can be always the same even if the value inside is changing).
I already started to extract the concept of interceptable arrays for this from mobx. Mstadministration could act as box for everything, and maybe be renamed to simply node for that purpose
@mweststrate Only interceptable ones or also faux arrays? :)
Could this also make it possible to have null references, like this:
export default types.model('Tasks, {
activeTask: types.maybe(types.reference(Task)),
}
At the moment this seems to throw an error:
union.js:41 Uncaught TypeError: type.is is not a function
Unless I'm missing something?
@fabiosussetto Yeah, that's why this issue is open. At the moment the reference is treated as a special thing, the idea behind making all complex is to also support things like that. Basically, each type will have on the type administration a get() method to read the box value, a set(value, path) to set the value of the box at the specified path, and an observe and intercept.
@mattiamanzati thanks, is there a possible workaround to get nullable references to work at the moment? For my example above I had to store the active task id (instead of the task reference) and then get the task myself with a getter.
Boxing might help here: types.maybe(types.model("TaskReference", { types.reference(Task) })), although that might in practice be more ugly than the solution you propose
@mweststrate thanks, I thought about the boxing approach you suggest but I got a strange error when I tried to create my "intermediate" boxing model:
const Task = types.model('Task', {
id: types.identifier(),
position: types.optional(types.number, 0),
content: types.string,
createdAt: types.string
});
const Lane = types.model('Lane', {
id: types.identifier(),
title: types.string,
tasks: types.optional(types.array(Task), [])
});
// This is my custom boxing model
const ActiveTask = types.model('ActiveTask', {
task: types.reference(Task),
});
const Taskboard = types.model('Taskboard', {
id: types.identifier(),
lanes: types.array(Lane),
activeTask: types.maybe(ActiveTask)
}, {
setActiveTask (task) {
// This line throws an error:
this.activeTask = { task };
}
});
When I try to call setActiveTask, I get:
utils.js:14 Uncaught Error: [mobx-state-tree] Failed to assign a value to a reference; the value should already be part of the same model tree
However the task I'm passing is definitely in the tree already (as I'm rendering it), also if I debug that line I can see the output of task.$treenode.path is
"/taskboards/entries/e8be04dc-45e3-420a-846d-aa08fc7bdc1d/lanes/0/tasks/0" (which seems ok to me)
Any idea why?
Yeah references are still a bit edgy at the point :unamused:. Namespaced references are a bit safer to use; types.reference(Task, "../tasks")
Thanks, I appreciate some bits are still work in progress.
Just so I understand if this is expected behaviour or me doing something wrong, I tried to use a namespaced ref insted:
const ActiveTask = types.model('ActiveTask', {
task: types.reference(Task, '../lanes/0/tasks'),
});
Apart from the fact I don't really know the index of the lane that task belongs to in the path unless I find it myself by looping over them (so I hardcoded '../lanes/0/tasks', how would this work?), I get:
Could not resolve '..' in '/../lanes/0', path of the patch does not resolve
Thanks again for your help.
Good questions! I think at the moment the safest approach is to go for a manual get / set with backing id(s).
Maybe we should also consider supporting types.reference(Task, get: () =>Tast, set: (v: any) => Task) ?
Implemented in #152 and out in 0.7.0