Skip to content

Commit a147023

Browse files
spacejackdead-claudia
authored andcommitted
Docs - prioritize closure components for state (MithrilJS#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
1 parent 4ac33fa commit a147023

File tree

6 files changed

+194
-82
lines changed

6 files changed

+194
-82
lines changed

docs/autoredraw.md

+11-7
Original file line numberDiff line numberDiff line change
@@ -96,13 +96,17 @@ Mithril also does not redraw after lifecycle methods. Parts of the UI may be red
9696
If you need to explicitly trigger a redraw within a lifecycle method, you should call `m.redraw()`, which will trigger an asynchronous redraw.
9797

9898
```javascript
99-
var StableComponent = {
100-
oncreate: function(vnode) {
101-
vnode.state.height = vnode.dom.offsetHeight
102-
m.redraw()
103-
},
104-
view: function() {
105-
return m("div", "This component is " + vnode.state.height + "px tall")
99+
function StableComponent() {
100+
var height = 0
101+
102+
return {
103+
oncreate: function(vnode) {
104+
height = vnode.dom.offsetHeight
105+
m.redraw()
106+
},
107+
view: function() {
108+
return m("div", "This component is " + height + "px tall")
109+
}
106110
}
107111
}
108112
```

docs/change-log.md

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

4849
#### Bug fixes
4950

docs/components.md

+138-39
Original file line numberDiff line numberDiff line change
@@ -103,14 +103,63 @@ To learn more about lifecycle methods, [see the lifecycle methods page](lifecycl
103103

104104
### Syntactic variants
105105

106+
#### Closure components
107+
108+
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.
109+
110+
```javascript
111+
function ClosureComponent(initialVnode) {
112+
// Each instance of this component has its own instance of `kind`
113+
var kind = "closure component"
114+
115+
return {
116+
view: function(vnode) {
117+
return m("div", "Hello from a " + kind)
118+
},
119+
oncreate: function(vnode) {
120+
console.log("We've created a " + kind)
121+
}
122+
}
123+
}
124+
```
125+
126+
The returned object must hold a `view` function, used to get the tree to render.
127+
128+
They can be consumed in the same way regular components can.
129+
130+
```javascript
131+
// EXAMPLE: via m.render
132+
m.render(document.body, m(ClosureComponent))
133+
134+
// EXAMPLE: via m.mount
135+
m.mount(document.body, ClosureComponent)
136+
137+
// EXAMPLE: via m.route
138+
m.route(document.body, "/", {
139+
"/": ClosureComponent
140+
})
141+
142+
// EXAMPLE: component composition
143+
function AnotherClosureComponent() {
144+
return {
145+
view: function() {
146+
return m("main",
147+
m(ClosureComponent)
148+
)
149+
}
150+
}
151+
}
152+
```
153+
154+
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.
155+
106156
#### ES6 classes
107157

108158
Components can also be written using ES6 class syntax:
109159

110160
```javascript
111161
class ES6ClassComponent {
112162
constructor(vnode) {
113-
// vnode.state is undefined at this point
114163
this.kind = "ES6 class"
115164
}
116165
view() {
@@ -148,65 +197,110 @@ class AnotherES6ClassComponent {
148197
}
149198
```
150199

151-
#### Closure components
200+
#### Mixing component kinds
201+
202+
Components can be freely mixed. A class component can have closure or POJO components as children, etc...
203+
204+
---
152205

153-
Functionally minded developers may prefer using the "closure component" syntax:
206+
### State
207+
208+
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.
209+
210+
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.
211+
212+
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.
213+
214+
#### Closure Component State
215+
216+
With a closure component state can simply be maintained by variables that are declared within the outer function. For example:
154217

155218
```javascript
156-
function closureComponent(vnode) {
157-
// vnode.state is undefined at this point
158-
var kind = "closure component"
219+
function ComponentWithState() {
220+
// Variables that hold component state
221+
var count = 0
159222

160223
return {
161224
view: function() {
162-
return m("div", "Hello from a " + kind)
163-
},
164-
oncreate: function() {
165-
console.log("We've created a " + kind)
225+
return m("div",
226+
m("p", "Count: " + count),
227+
m("button", {
228+
onclick: function() {
229+
count += 1
230+
}
231+
}, "Increment count")
232+
)
166233
}
167234
}
168235
}
169236
```
170237

171-
The returned object must hold a `view` function, used to get the tree to render.
172-
173-
They can be consumed in the same way regular components can.
238+
Any functions declared within the closure also have access to its state variables.
174239

175240
```javascript
176-
// EXAMPLE: via m.render
177-
m.render(document.body, m(closureComponent))
241+
function ComponentWithState() {
242+
var count = 0
178243

179-
// EXAMPLE: via m.mount
180-
m.mount(document.body, closureComponent)
244+
function increment() {
245+
count += 1
246+
}
181247

182-
// EXAMPLE: via m.route
183-
m.route(document.body, "/", {
184-
"/": closureComponent
185-
})
248+
function decrement() {
249+
count -= 1
250+
}
186251

187-
// EXAMPLE: component composition
188-
function anotherClosureComponent() {
189252
return {
190253
view: function() {
191-
return m("main", [
192-
m(closureComponent)
193-
])
254+
return m("div",
255+
m("p", "Count: " + count),
256+
m("button", {
257+
onclick: increment
258+
}, "Increment"),
259+
m("button", {
260+
onclick: decrement
261+
}, "Decrement")
262+
)
194263
}
195264
}
196265
}
197266
```
198267

199-
#### Mixing component kinds
268+
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.
200269

201-
Components can be freely mixed. A Class component can have closure or POJO components as children, etc...
270+
#### Class Component State
202271

203-
---
272+
With classes, state can be managed by class instance properties and methods. For example:
204273

205-
### State
274+
```javascript
275+
class ComponentWithState() {
276+
constructor() {
277+
this.count = 0
278+
}
279+
increment() {
280+
this.count += 1
281+
}
282+
decrement() {
283+
this.count -= 1
284+
}
285+
view() {
286+
return m("div",
287+
m("p", "Count: " + count),
288+
m("button", {
289+
onclick: () => {this.increment()}
290+
}, "Increment"),
291+
m("button", {
292+
onclick: () => {this.decrement()}
293+
}, "Decrement")
294+
)
295+
}
296+
}
297+
```
206298

207-
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.
299+
Note that we must wrap the event callbacks in arrow functions so that the `this` context is preserved correctly.
300+
301+
#### POJO Component State
208302

209-
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.
303+
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.
210304

211305
#### At initialization
212306

@@ -228,10 +322,6 @@ m(ComponentWithInitialState)
228322
// <div>Initial content</div>
229323
```
230324

231-
For class components, the state is an instance of the class, set right after the constructor is called.
232-
233-
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).
234-
235325
#### Via vnode.state
236326

237327
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")
351441
var Login = {
352442
view: function() {
353443
return m(".login", [
354-
m("input[type=text]", {oninput: m.withAttr("value", Auth.setUsername), value: Auth.username}),
355-
m("input[type=password]", {oninput: m.withAttr("value", Auth.setPassword), value: Auth.password}),
356-
m("button", {disabled: !Auth.canSubmit(), onclick: Auth.login}, "Login"),
444+
m("input[type=text]", {
445+
oninput: m.withAttr("value", Auth.setUsername),
446+
value: Auth.username
447+
}),
448+
m("input[type=password]", {
449+
oninput: m.withAttr("value", Auth.setPassword),
450+
value: Auth.password
451+
}),
452+
m("button", {
453+
disabled: !Auth.canSubmit(),
454+
onclick: Auth.login
455+
}, "Login")
357456
])
358457
}
359458
}

docs/hyperscript.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -539,7 +539,7 @@ Instead, prefer using Javascript expressions such as the ternary operator and Ar
539539
```javascript
540540
// PREFER
541541
var BetterListComponent = {
542-
view: function() {
542+
view: function(vnode) {
543543
return m("ul", vnode.attrs.items.map(function(item) {
544544
return m("li", item)
545545
}))

docs/lifecycle-methods.md

+34-26
Original file line numberDiff line numberDiff line change
@@ -60,19 +60,24 @@ Like in other hooks, the `this` keyword in the `oninit` callback points to `vnod
6060
The `oninit` hook is useful for initializing component state based on arguments passed via `vnode.attrs` or `vnode.children`.
6161

6262
```javascript
63-
var ComponentWithState = {
64-
oninit: function(vnode) {
65-
this.data = vnode.attrs.data
66-
},
67-
view: function() {
68-
return m("div", this.data) // displays data from initialization time
63+
function ComponentWithState() {
64+
var initialData
65+
return {
66+
oninit: function(vnode) {
67+
initialData = vnode.attrs.data
68+
},
69+
view: function(vnode) {
70+
return [
71+
// displays data from initialization time:
72+
m("div", "Initial: " + initialData),
73+
// displays current data:
74+
m("div", "Current: " + vnode.attrs.data)
75+
]
76+
}
6977
}
7078
}
7179

7280
m(ComponentWithState, {data: "Hello"})
73-
74-
// Equivalent HTML
75-
// <div>Hello</div>
7681
```
7782

7883
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,17 +115,19 @@ The `onupdate(vnode)` hook is called after a DOM element is updated, while attac
110115

111116
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.
112117

113-
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.
118+
DOM elements whose vnodes have an `onupdate` hook do not get recycled.
114119

115120
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.
116121

117122
```javascript
118-
var RedrawReporter = {
119-
count: 0,
120-
onupdate: function(vnode) {
121-
console.log("Redraws so far: ", ++vnode.state.count)
122-
},
123-
view: function() {}
123+
function RedrawReporter() {
124+
var count = 0
125+
return {
126+
onupdate: function() {
127+
console.log("Redraws so far: ", ++count)
128+
},
129+
view: function() {}
130+
}
124131
}
125132

126133
m(RedrawReporter, {data: "Hello"})
@@ -163,16 +170,17 @@ Like in other hooks, the `this` keyword in the `onremove` callback points to `vn
163170
The `onremove` hook is useful for running clean up tasks.
164171

165172
```javascript
166-
var Timer = {
167-
oninit: function(vnode) {
168-
this.timeout = setTimeout(function() {
169-
console.log("timed out")
170-
}, 1000)
171-
},
172-
onremove: function(vnode) {
173-
clearTimeout(this.timeout)
174-
},
175-
view: function() {}
173+
function Timer() {
174+
var timeout = setTimeout(function() {
175+
console.log("timed out")
176+
}, 1000)
177+
178+
return {
179+
onremove: function() {
180+
clearTimeout(timeout)
181+
},
182+
view: function() {}
183+
}
176184
}
177185
```
178186

docs/prop.md

+9-9
Original file line numberDiff line numberDiff line change
@@ -54,15 +54,15 @@ The `m.prop` method creates a prop, a getter/setter object wrapping a single mut
5454
In conjunction with [`m.withAttr`](withAttr.md), you can emulate two-way binding pretty easily.
5555

5656
```javascript
57-
var Component = {
58-
oninit: function(vnode) {
59-
vnode.state.current = m.prop("")
60-
},
61-
view: function(vnode) {
62-
return m("input", {
63-
oninput: m.withAttr("value", vnode.state.current.set),
64-
value: vnode.state.current.get(),
65-
})
57+
function Component() {
58+
var current = m.prop("")
59+
return {
60+
view: function(vnode) {
61+
return m("input", {
62+
oninput: m.withAttr("value", current.set),
63+
value: current.get(),
64+
})
65+
}
6666
}
6767
}
6868
```

0 commit comments

Comments
 (0)