|
| 1 | + |
| 2 | +### Creating models |
| 3 | + |
| 4 | +<i><a style="color: white; background:cornflowerblue;padding:5px;margin:5px;border-radius:2px" href="https://egghead.io/lessons/react-describe-your-application-domain-using-mobx-state-tree-mst-models">egghead.io lesson 1: Describe Your Application Domain Using mobx-state-tree(MST) Models</a></i> |
| 5 | + |
| 6 | +The most important type in MST is `types.model`, which can be used to describe the shape of an object. |
| 7 | +An example: |
| 8 | + |
| 9 | +```javascript |
| 10 | +const TodoStore = types |
| 11 | + // 1 |
| 12 | + .model("TodoStore", { |
| 13 | + loaded: types.boolean, // 2 |
| 14 | + endpoint: "http://localhost", // 3 |
| 15 | + todos: types.array(Todo), // 4 |
| 16 | + selectedTodo: types.reference(Todo) // 5 |
| 17 | + }) |
| 18 | + .views(self => { |
| 19 | + return { |
| 20 | + // 6 |
| 21 | + get completedTodos() { |
| 22 | + return self.todos.filter(t => t.done) |
| 23 | + }, |
| 24 | + // 7 |
| 25 | + findTodosByUser(user) { |
| 26 | + return self.todos.filter(t => t.assignee === user) |
| 27 | + } |
| 28 | + } |
| 29 | + }) |
| 30 | + .actions(self => { |
| 31 | + return { |
| 32 | + addTodo(title) { |
| 33 | + self.todos.push({ |
| 34 | + id: Math.random(), |
| 35 | + title |
| 36 | + }) |
| 37 | + } |
| 38 | + } |
| 39 | + }) |
| 40 | +``` |
| 41 | + |
| 42 | +When defining a model, it is advised to give the model a name for debugging purposes (see `// 1`). |
| 43 | +A model takes additionally object argument defining the properties. |
| 44 | + |
| 45 | +The _properties_ argument is a key-value set where each key indicates the introduction of a property, and the value its type. The following types are acceptable: |
| 46 | + |
| 47 | +1. A type. This can be a simple primitive type like `types.boolean`, see `// 2`, or a complex, possibly pre-defined type (`// 4`) |
| 48 | +2. A primitive. Using a primitive as type is syntactic sugar for introducing a property with a default value. See `// 3`, `endpoint: "http://localhost"` is the same as `endpoint: types.optional(types.string, "http://localhost")`. The primitive type is inferred from the default value. Properties with a default value can be omitted in snapshots. |
| 49 | +3. A [computed property](https://mobx.js.org/refguide/computed-decorator.html), see `// 6`. Computed properties are tracked and memoized by MobX. Computed properties will not be stored in snapshots or emit patch events. It is possible to provide a setter for a computed property as well. A setter should always invoke an action. |
| 50 | +4. A view function (see `// 7`). A view function can, unlike computed properties, take arbitrary arguments. It won't be memoized, but its value can be tracked by MobX nonetheless. View functions are not allowed to change the model, but should rather be used to retrieve information from the model. |
| 51 | + |
| 52 | +_Tip: `(self) => ({ action1() { }, action2() { }})` is ES6 syntax for `function (self) { return { action1: function() { }, action2: function() { } }}`. In other words, it's short way of directly returning an object literal. |
| 53 | +For that reason a comma between each member of a model is mandatory, unlike classes which are syntactically a totally different concept._ |
| 54 | + |
| 55 | +`types.model` creates a chainable model type, where each chained method produces a new type: |
| 56 | + |
| 57 | +- `.named(name)` clones the current type, but gives it a new name |
| 58 | +- `.props(props)` produces a new type, based on the current one, and adds / overrides the specified properties |
| 59 | +- `.actions(self => object literal with actions)` produces a new type, based on the current one, and adds / overrides the specified actions |
| 60 | +- `.views(self => object literal with view functions)` produces a new type, based on the current one, and adds / overrides the specified view functions |
| 61 | +- `.preProcessSnapshot(snapshot => snapshot)` can be used to pre-process the raw JSON before instantiating a new model. See [Lifecycle hooks](#lifecycle-hooks-for-typesmodel) or alternatively `types.snapshotProcessor` |
| 62 | +- `.postProcessSnapshot(snapshot => snapshot)` can be used to post-process the raw JSON before getting a model snapshot. See [Lifecycle hooks](#lifecycle-hooks-for-typesmodel) or alternatively `types.snapshotProcessor` |
| 63 | + |
| 64 | +Note that `views` and `actions` don't define actions and views directly, but rather they should be given a function. |
| 65 | +The function will be invoked when a new model instance is created. The instance will be passed in as the first and only argument typically called `self`. |
| 66 | +This has two advantages: |
| 67 | + |
| 68 | +1. All methods will always be bound correctly, and won't suffer from an unbound `this` |
| 69 | +2. The closure can be used to store private state or methods of the instance. See also [actions](#actions) and [volatile state](#volatile-state). |
| 70 | + |
| 71 | +Quick example: |
| 72 | + |
| 73 | +```javascript |
| 74 | +const TodoStore = types |
| 75 | + .model("TodoStore", { |
| 76 | + /* props */ |
| 77 | + }) |
| 78 | + .actions(self => { |
| 79 | + const instantiationTime = Date.now() |
| 80 | + |
| 81 | + function addTodo(title) { |
| 82 | + console.log(`Adding Todo ${title} after ${(Date.now() - instantiationTime) / 1000}s.`) |
| 83 | + self.todos.push({ |
| 84 | + id: Math.random(), |
| 85 | + title |
| 86 | + }) |
| 87 | + } |
| 88 | + |
| 89 | + return { addTodo } |
| 90 | + }) |
| 91 | +``` |
| 92 | + |
| 93 | +It is perfectly fine to chain multiple `views`, `props` calls etc in arbitrary order. This can be a great way to structure complex types, mix-in utility functions, etc. Each call in the chain creates a new, immutable type which can itself be stored and reused as part of other types, etc. |
| 94 | + |
| 95 | +It is also possible to define lifecycle hooks in the _actions_ object. These are actions with a predefined name that are run at a specific moment. See [Lifecycle hooks](#lifecycle-hooks-for-typesmodel). |
0 commit comments