Docs - prioritize closure components for state (#2292)

* Emphasize closure components in components.md

* Use closure components for all stateful component examples

* Add change-log entry

* Edits and separate sections for closure, class & POJO state
This commit is contained in:
spacejack 2018-11-13 01:04:04 -05:00 committed by Isiah Meadows
parent 4ac33fa483
commit a147023f4e
6 changed files with 213 additions and 101 deletions

View file

@ -96,13 +96,17 @@ Mithril also does not redraw after lifecycle methods. Parts of the UI may be red
If you need to explicitly trigger a redraw within a lifecycle method, you should call `m.redraw()`, which will trigger an asynchronous redraw.
```javascript
var StableComponent = {
function StableComponent() {
var height = 0
return {
oncreate: function(vnode) {
vnode.state.height = vnode.dom.offsetHeight
height = vnode.dom.offsetHeight
m.redraw()
},
view: function() {
return m("div", "This component is " + vnode.state.height + "px tall")
return m("div", "This component is " + height + "px tall")
}
}
}
```

View file

@ -44,6 +44,7 @@
- render/core: remove the DOM nodes recycling pool ([#2122](https://github.com/MithrilJS/mithril.js/pull/2122))
- render/core: revamp the core diff engine, and introduce a longest-increasing-subsequence-based logic to minimize DOM operations when re-ordering keyed nodes.
- API: Introduction of `m.prop()` ([#2268](https://github.com/MithrilJS/mithril.js/pull/2268))
- docs: Emphasize Closure Components for stateful components, use them for all stateful component examples.
#### Bug fixes

View file

@ -103,6 +103,56 @@ To learn more about lifecycle methods, [see the lifecycle methods page](lifecycl
### Syntactic variants
#### Closure components
One of the easiest ways to manage state in a component is with a closure. A "closure component" is one that returns an object with a view function and optionally other lifecycle hooks. It has the ability to manage instance state within the body of the outer function.
```javascript
function ClosureComponent(initialVnode) {
// Each instance of this component has its own instance of `kind`
var kind = "closure component"
return {
view: function(vnode) {
return m("div", "Hello from a " + kind)
},
oncreate: function(vnode) {
console.log("We've created a " + kind)
}
}
}
```
The returned object must hold a `view` function, used to get the tree to render.
They can be consumed in the same way regular components can.
```javascript
// EXAMPLE: via m.render
m.render(document.body, m(ClosureComponent))
// EXAMPLE: via m.mount
m.mount(document.body, ClosureComponent)
// EXAMPLE: via m.route
m.route(document.body, "/", {
"/": ClosureComponent
})
// EXAMPLE: component composition
function AnotherClosureComponent() {
return {
view: function() {
return m("main",
m(ClosureComponent)
)
}
}
}
```
If a component does *not* have state then you should opt for the simpler POJO component to avoid the additional overhead and boilerplate of the closure.
#### ES6 classes
Components can also be written using ES6 class syntax:
@ -110,7 +160,6 @@ Components can also be written using ES6 class syntax:
```javascript
class ES6ClassComponent {
constructor(vnode) {
// vnode.state is undefined at this point
this.kind = "ES6 class"
}
view() {
@ -148,57 +197,9 @@ class AnotherES6ClassComponent {
}
```
#### Closure components
Functionally minded developers may prefer using the "closure component" syntax:
```javascript
function closureComponent(vnode) {
// vnode.state is undefined at this point
var kind = "closure component"
return {
view: function() {
return m("div", "Hello from a " + kind)
},
oncreate: function() {
console.log("We've created a " + kind)
}
}
}
```
The returned object must hold a `view` function, used to get the tree to render.
They can be consumed in the same way regular components can.
```javascript
// EXAMPLE: via m.render
m.render(document.body, m(closureComponent))
// EXAMPLE: via m.mount
m.mount(document.body, closureComponent)
// EXAMPLE: via m.route
m.route(document.body, "/", {
"/": closureComponent
})
// EXAMPLE: component composition
function anotherClosureComponent() {
return {
view: function() {
return m("main", [
m(closureComponent)
])
}
}
}
```
#### Mixing component kinds
Components can be freely mixed. A Class component can have closure or POJO components as children, etc...
Components can be freely mixed. A class component can have closure or POJO components as children, etc...
---
@ -206,7 +207,100 @@ Components can be freely mixed. A Class component can have closure or POJO compo
Like all virtual DOM nodes, component vnodes can have state. Component state is useful for supporting object-oriented architectures, for encapsulation and for separation of concerns.
The state of a component can be accessed three ways: as a blueprint at initialization, via `vnode.state` and via the `this` keyword in component methods.
Note that unlike many other frameworks, component state does *not* trigger [redraws](autoredraw.md) or DOM updates. Instead, redraws are performed when event handlers fire, when HTTP requests made by [m.request](request.md) complete or when the browser navigates to different routes. Mithril's component state mechanisms simply exist as a convenience for applications.
If a state change occurs that is not as a result of any of the above conditions (e.g. after a `setTimeout`), then you can use `m.redraw()` to trigger a redraw manually.
#### Closure Component State
With a closure component state can simply be maintained by variables that are declared within the outer function. For example:
```javascript
function ComponentWithState() {
// Variables that hold component state
var count = 0
return {
view: function() {
return m("div",
m("p", "Count: " + count),
m("button", {
onclick: function() {
count += 1
}
}, "Increment count")
)
}
}
}
```
Any functions declared within the closure also have access to its state variables.
```javascript
function ComponentWithState() {
var count = 0
function increment() {
count += 1
}
function decrement() {
count -= 1
}
return {
view: function() {
return m("div",
m("p", "Count: " + count),
m("button", {
onclick: increment
}, "Increment"),
m("button", {
onclick: decrement
}, "Decrement")
)
}
}
}
```
A big advantage of closure components is that we don't need to worry about binding `this` when attaching event handler callbacks. In fact `this` is never used at all and we never have to think about `this` context ambiguities.
#### Class Component State
With classes, state can be managed by class instance properties and methods. For example:
```javascript
class ComponentWithState() {
constructor() {
this.count = 0
}
increment() {
this.count += 1
}
decrement() {
this.count -= 1
}
view() {
return m("div",
m("p", "Count: " + count),
m("button", {
onclick: () => {this.increment()}
}, "Increment"),
m("button", {
onclick: () => {this.decrement()}
}, "Decrement")
)
}
}
```
Note that we must wrap the event callbacks in arrow functions so that the `this` context is preserved correctly.
#### POJO Component State
For POJO components the state of a component can be accessed three ways: as a blueprint at initialization, via `vnode.state` and via the `this` keyword in component methods.
#### At initialization
@ -228,10 +322,6 @@ m(ComponentWithInitialState)
// <div>Initial content</div>
```
For class components, the state is an instance of the class, set right after the constructor is called.
For closure components, the state is the object returned by the closure, set right after the closure returns. The state object is mostly redundant for closure components (since variables defined in the closure scope can be used instead).
#### Via vnode.state
State can also be accessed via the `vnode.state` property, which is available to all lifecycle methods as well as the `view` method of a component.
@ -351,9 +441,18 @@ var Auth = require("../models/Auth")
var Login = {
view: function() {
return m(".login", [
m("input[type=text]", {oninput: m.withAttr("value", Auth.setUsername), value: Auth.username}),
m("input[type=password]", {oninput: m.withAttr("value", Auth.setPassword), value: Auth.password}),
m("button", {disabled: !Auth.canSubmit(), onclick: Auth.login}, "Login"),
m("input[type=text]", {
oninput: m.withAttr("value", Auth.setUsername),
value: Auth.username
}),
m("input[type=password]", {
oninput: m.withAttr("value", Auth.setPassword),
value: Auth.password
}),
m("button", {
disabled: !Auth.canSubmit(),
onclick: Auth.login
}, "Login")
])
}
}

View file

@ -539,7 +539,7 @@ Instead, prefer using Javascript expressions such as the ternary operator and Ar
```javascript
// PREFER
var BetterListComponent = {
view: function() {
view: function(vnode) {
return m("ul", vnode.attrs.items.map(function(item) {
return m("li", item)
}))

View file

@ -60,19 +60,24 @@ Like in other hooks, the `this` keyword in the `oninit` callback points to `vnod
The `oninit` hook is useful for initializing component state based on arguments passed via `vnode.attrs` or `vnode.children`.
```javascript
var ComponentWithState = {
function ComponentWithState() {
var initialData
return {
oninit: function(vnode) {
this.data = vnode.attrs.data
initialData = vnode.attrs.data
},
view: function() {
return m("div", this.data) // displays data from initialization time
view: function(vnode) {
return [
// displays data from initialization time:
m("div", "Initial: " + initialData),
// displays current data:
m("div", "Current: " + vnode.attrs.data)
]
}
}
}
m(ComponentWithState, {data: "Hello"})
// Equivalent HTML
// <div>Hello</div>
```
You should not modify model data synchronously from this method. Since `oninit` makes no guarantees regarding the status of other elements, model changes created from this method may not be reflected in all parts of the UI until the next render cycle.
@ -110,18 +115,20 @@ The `onupdate(vnode)` hook is called after a DOM element is updated, while attac
This hook is only called if the element existed in the previous render cycle. It is not called when an element is created or when it is recycled.
Like in other hooks, the `this` keyword in the `onupdate` callback points to `vnode.state`. DOM elements whose vnodes have an `onupdate` hook do not get recycled.
DOM elements whose vnodes have an `onupdate` hook do not get recycled.
The `onupdate` hook is useful for reading layout values that may trigger a repaint, and for dynamically updating UI-affecting state in third party libraries after model data has been changed.
```javascript
var RedrawReporter = {
count: 0,
onupdate: function(vnode) {
console.log("Redraws so far: ", ++vnode.state.count)
function RedrawReporter() {
var count = 0
return {
onupdate: function() {
console.log("Redraws so far: ", ++count)
},
view: function() {}
}
}
m(RedrawReporter, {data: "Hello"})
```
@ -163,17 +170,18 @@ Like in other hooks, the `this` keyword in the `onremove` callback points to `vn
The `onremove` hook is useful for running clean up tasks.
```javascript
var Timer = {
oninit: function(vnode) {
this.timeout = setTimeout(function() {
function Timer() {
var timeout = setTimeout(function() {
console.log("timed out")
}, 1000)
},
onremove: function(vnode) {
clearTimeout(this.timeout)
return {
onremove: function() {
clearTimeout(timeout)
},
view: function() {}
}
}
```
---

View file

@ -54,17 +54,17 @@ The `m.prop` method creates a prop, a getter/setter object wrapping a single mut
In conjunction with [`m.withAttr`](withAttr.md), you can emulate two-way binding pretty easily.
```javascript
var Component = {
oninit: function(vnode) {
vnode.state.current = m.prop("")
},
function Component() {
var current = m.prop("")
return {
view: function(vnode) {
return m("input", {
oninput: m.withAttr("value", vnode.state.current.set),
value: vnode.state.current.get(),
oninput: m.withAttr("value", current.set),
value: current.get(),
})
}
}
}
```
They're also useful for making simpler models.