forked from CyclejsCN/cyclejs.cn
-
Notifications
You must be signed in to change notification settings - Fork 0
/
components.html
717 lines (564 loc) · 27.7 KB
/
components.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
<!doctype html>
<html>
<head>
<meta charset='utf-8'>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width">
<title>Cycle.js - Components</title>
<!-- Flatdoc -->
<script src='support/vendor/jquery.js'></script>
<script src='support/vendor/highlight.pack.js'></script>
<script src='legacy.js'></script>
<script src='flatdoc.js'></script>
<!-- Algolia's DocSearch main theme -->
<link href='//cdn.jsdelivr.net/docsearch.js/2/docsearch.min.css' rel='stylesheet' />
<!-- Others -->
<script async src="//static.jsbin.com/js/embed.js"></script>
<!-- Flatdoc theme -->
<link href='theme/style.css' rel='stylesheet'>
<script src='theme/script.js'></script>
<link href='support/vendor/highlight-github-gist.css' rel='stylesheet'>
<!-- Meta -->
<meta content="Cycle.js - Components" property="og:title">
<meta content="一个函数式和响应式的 JavaScript 框架,用来编写前瞻性代码。" name="description">
<!-- Content -->
<script id="markdown" type="text/markdown" src="index.html">
# Components
## Automatically reusable
User interfaces are usually made up of many reusable pieces: buttons, charts, sliders, hoverable avatars, smart form fields, etc. In many frameworks, including Cycle.js, these are called components. However, in this framework they have a special property.
**Any Cycle.js app can be reused as a component in a larger Cycle.js app.**
How is that so? In any framework you can build a program that just makes one slider. By now, we know how to make a Cycle.js `main()` that makes a smart slider widget. Then, since `main()` is just a function taking inputs from the external world and generating outputs in return, we can just call that function inside a larger Cycle.js app.
Each of these "small Cycle.js `main()` functions" are called **dataflow components**. The sources which a dataflow component receives are streams provided by its parent, and sinks are streams given back to the parent. All along, we have been building dataflow components, because the `main()` given to `run(main, drivers)` is also a dataflow component. Its parent are the drivers, because that is where its sources come from and where its sinks go to.
![dataflow component](img/dataflow-component.svg)
To learn by doing, let's just make a dataflow component for a single labeled slider. It should take user events as input, and generate a virtual DOM stream of a slider element. Besides the virtual DOM, it might also output a value: the stream of slider values. It might also take attributes (from its parent) as input to customize some behavior or looks. These are sometimes called *props* ("properties") in other frameworks.
## A labeled slider component
A labeled slider has two parts: a label and slider, side by side, where the label always displays the current dynamic value of the slider.
<a class="jsbin-embed" href="//jsbin.com/napoke/embed?output">JS Bin on jsbin.com</a>
Every labeled slider has some properties:
- Label text (`'Weight'`, `'Height'`, etc)
- Unit text (`'kg'`, `'cm'`, etc)
- Min value
- Max value
- Initial value
These props can be encoded as an object, wrapped in a stream, and passed to our `main()` function as a *source* input:
```javascript
function main(sources) {
const props$ = sources.props;
// ...
return sinks;
}
```
To use this main function, we call `run`:
```javascript
run(main, {
props: () => xs.of({
label: 'Weight', unit: 'kg', min: 40, value: 70, max: 140
}),
DOM: makeDOMDriver('#app')
});
```
Remember that even though we are building a component, we are assuming our labeled slider to be our main program. Then, because `props` are an input given to the labeled slider from its parent, the only parent of `main()` in this case is `run`. That is why we need to configure `props` as a fake driver.
The other input to this labeled slider program is the DOM source representing user events:
```diff
function main(sources) {
+ const domSource = sources.DOM;
const props$ = sources.props;
// ...
return sinks;
}
```
The remainder of the program is rather easy given that we've written the same labeled slider in the two previous chapters. However, this time we take props as the initial value.
```javascript
function main(sources) {
const domSource = sources.DOM;
const props$ = sources.props;
const newValue$ = domSource
.select('.slider')
.events('input')
.map(ev => ev.target.value);
const state$ = props$
.map(props => newValue$
.map(val => ({
label: props.label,
unit: props.unit,
min: props.min,
value: val,
max: props.max
}))
.startWith(props)
)
.flatten()
.remember();
const vdom$ = state$
.map(state =>
div('.labeled-slider', [
span('.label',
state.label + ' ' + state.value + state.unit
),
input('.slider', {
attrs: {type: 'range', min: state.min, max: state.max, value: state.value}
})
])
);
const sinks = {
DOM: vdom$,
value: state$.map(state => state.value),
};
return sinks;
}
```
You might have noticed that besides the virtual DOM output, we also return the `value$` stream as a sink:
```diff
// ...
const sinks = {
DOM: vdom$,
+ value: value$,
};
return sinks;
}
```
This value stream is important as a sink if the parent wishes to use the numeric value for some calculations, such as that of BMI. In the program we wrote above, the parent of `main()` are the drivers. The drivers don't need to use `value$`, that's why we don't need a driver named `value`. However, when the parent of the slider component is another dataflow component, like in the next section, then `value$` will be important.
> How to name sources/sinks?
>
> You may have noticed that we chose the name `value` as a sink, not `value$`. Does this contradict our convention that streams should always be suffixed with `$`? Not particularly.
>
> Sources and sinks are an exception because they are special sockets that connect the internals of your component with the external world. Their names are just "keys" used to put or get streams. In the case of `main`, those keys need to match the same keys you gave to the `drivers` object in `run(main, drivers)`. Notice how each driver indexed by a key in the `drivers` object *is not* a stream. They are functions, because drivers are functions.
>
> This is why we shouldn't give the name `DOM$`, because in the `drivers` object, the value behind that key is a function (the DOM Driver), and in the `main` function, `sources.DOM` is the DOM Source object with methods like `select()` and `events()`.
>
> Try to maintain the convention that source and sink names are just *keys* in the sources object and sinks object. You may then "pick" your stream from the sources, like we did with `const props$ = sources.props;` for instance.
## Using a component
Now that our dataflow component for a labeled slider is ready, we can use it in the context of a larger application. First, we will rename our component to `LabeledSlider`, and `main()` will refer to our larger application.
```diff
-function main(sources) {
+function LabeledSlider(sources) {
const domSource = sources.DOM;
const props$ = sources.props;
// ...
return sinks;
}
+function main(sources) {
+ // Call LabeledSlider() here...
+}
```
Since `LabeledSlider` is just a function, we can call it with some sources to get its sinks as output.
```javascript
function main(sources) {
const props$ = xs.of({
label: 'Radius', unit: '', min: 10, value: 30, max: 100
});
const childSources = {DOM: sources.DOM, props: props$};
const labeledSlider = LabeledSlider(childSources);
const childVDom$ = labeledSlider.DOM;
const childValue$ = labeledSlider.value;
// ...
}
```
> Why name components with CapitalCase?
>
> You probably noticed we named the dataflow component as `LabeledSlider`. Usually in JavaScript, capitalized names are used for classes and constructor functions. Since Cycle.js uses functional programming techniques heavily, Object-oriented programming conventions are irrelevant, there are rarely (or never) classes in Cycle.js apps.
>
> For this reason, capitalized names become available in the functional programming flavor of JavaScript. We will follow the convention of capitalized names such as `FooButton` for dataflow components (in other words, small Cycle.js apps). Their camel-case counterpart such as `fooButton` will refer to the output of `FooButton` function when called, i.e., the sinks object.
Now we have `childVDom$` and `childValue$` as sinks from the labeled slider, available for use as regular streams in the context of the `main()` parent. We use `childValue$` to render a circle with radius equal to the slider's value, and use `childVDom$` to embed the slider's virtual DOM in the parent's virtual DOM:
```javascript
function main(sources) {
// ...
const childVDom$ = labeledSlider.DOM;
const childValue$ = labeledSlider.value;
const vdom$ = xs.combine(childValue$, childVDom$)
.map(([value, childVDom]) =>
div([
childVDom,
div({style: {
backgroundColor: '#58D3D8',
width: String(2 * value) + 'px',
height: String(2 * value) + 'px',
borderRadius: String(value) + 'px'
}})
])
);
return {
DOM: vdom$
};
}
```
As a result, we get a Cycle.js program where the labeled slider controls the size of a rendered circle.
<a class="jsbin-embed" href="//jsbin.com/yojoho/embed?output">JS Bin on jsbin.com</a>
## Isolating multiple instances
Our labeled sliders were originally built for the BMI example, so we should see how the component we just built can be used in the BMI example.
The naïve approach is to simply call `LabeledSlider()` twice, once with props for weight, and again with props for height:
<a class="jsbin-embed" href="//jsbin.com/lagegax/embed?output">JS Bin on jsbin.com</a>
```javascript
function main(sources) {
const weightProps$ = xs.of({
label: 'Weight', unit: 'kg', min: 40, value: 70, max: 150
});
const heightProps$ = xs.of({
label: 'Height', unit: 'cm', min: 140, value: 170, max: 210
});
const weightSources = {DOM: sources.DOM, props: weightProps$};
const heightSources = {DOM: sources.DOM, props: heightProps$};
const weightSlider = LabeledSlider(weightSources);
const heightSlider = LabeledSlider(heightSources);
const weightVDom$ = weightSlider.DOM;
const weightValue$ = weightSlider.value;
const heightVDom$ = heightSlider.DOM;
const heightValue$ = heightSlider.value;
const bmi$ = xs.combine(weightValue$, heightValue$)
.map(([weight, height]) => {
const heightMeters = height * 0.01;
const bmi = Math.round(weight / (heightMeters * heightMeters));
return bmi;
})
.remember();
const vdom$ = xs.combine(bmi$, weightVDom$, heightVDom$)
.map(([bmi, weightVDom, heightVDom]) =>
div([
weightVDom,
heightVDom,
h2('BMI is ' + bmi)
])
);
return {
DOM: vdom$
};
}
```
However, this creates a bug. Both labeled sliders change when any slider is moved. Can you see why? Pay attention to the implementation of `LabeledSlider` with this piece of code:
```javascript
function LabeledSlider(sources) {
// ...
const newValue$ = domSource
.select('.slider')
.events('input')
.map(ev => ev.target.value);
// ...
}
```
Suppose we just ran this function for the weight labeled slider. The line `sources.DOM.select('.slider')` **will attempt to select all** `.slider` **elements on the entire DOM tree managed by this app**. This means both the `.slider` in the weight component and the `.slider` in the height component. As a result, the weight component will detect changes to both the height slider and the weight slider, which is a bug.
A component should not leak its output to other components, and it should not be able to detect outputs from other sibling components. In order to keep the nice property of "a component is just a Cycle.js app", we want two properties:
- A component's **sources** are not affected by other components' sources.
- A component's **sinks** do not affect other components' sinks.
In order to achieve these properties, we need to modify the sources when they enter the component, and also modify the sinks when they are returned from the component. To make sources and sinks isolated from influence of other components, we need to introduce a scope for the current component.
For the DOM source and DOM sink, we can use a unique identifier string as namespace for the virtual DOM element. First, we patch the DOM sink, adding a className to the VNodes it emits.
```diff
function main(sources) {
// ...
const weightSlider = LabeledSlider(weightSources);
const heightSlider = LabeledSlider(heightSources);
const weightVDom$ = weightSlider.DOM
+ .map(vnode => {
+ vnode.sel += '.weight';
+ return vnode;
+ });
const weightValue$ = weightSlider.value;
const heightVDom$ = heightSlider.DOM
+ .map(vnode => {
+ vnode.sel += '.height';
+ return vnode;
+ });
const heightValue$ = heightSlider.value;
// ...
}
```
This will result in the following rendered HTML:
```html
<div class="labeled-slider weight">
<span class="label">Weight 70kg</span>
<input class="slider" type="range" min="40" max="150">
</div>
```
For querying user events on these rendered sliders, the `weightSlider` dataflow component should detect user events *only* from the `<div class="labeled-slider weight">` element and its descendants when the stream `sources.DOM.select('.slider').events('input')` is used.
In the context of the labeled slider component, **`sources.DOM.select()` should refer only to the elements that were created by the corresponding DOM sink in that component**.
We can achieve that by narrowing down the DOM source before it is given to the component, using the same className we patched on the sink, like this:
```diff
function main(sources) {
// ...
const weightSources = {
- DOM: sources.DOM,
+ DOM: sources.DOM.select('.weight'),
props: weightProps$
};
const heightSources = {
- DOM: sources.DOM,
+ DOM: sources.DOM.select('.height'),
props: heightProps$
};
const weightSlider = LabeledSlider(weightSources);
const heightSlider = LabeledSlider(heightSources);
// ...
}
```
> ### What does `select()` do?
>
> We have used `.select(selector).events(eventType)` many times previously to get a stream emitting DOM events of type `eventType` happening on the `selector` element(s).
>
> In the code above, `sources.DOM` is a so-called "DOM Source", an object with some functions attached that help us query for the correct event stream. We also called `sources.DOM.select(selector)` without `.events(eventType)`, which returns a **new** DOM source, on which we can call again `select()` or `events()`.
>
> `select('.foo').select('.bar').events('click')` returns a stream of click events happening on `'.foo .bar'` elements. In other words, these are all clicks happening on `'.bar'` elements descendants of `'.foo'` elements. The first call, `select('.foo')`, allows us to "narrow down" the scope of the DOM source.
The code we wrote for isolating sources and sinks looks like boilerplate. Ideally we want to avoid manually managing scopes for each component instance using classNames:
```javascript
function main(sources) {
// ...
const weightSources = {
DOM: sources.DOM.select('.weight'), props: weightProps$
};
const heightSources = {
DOM: sources.DOM.select('.height'), props: heightProps$
};
// ...
const weightVDom$ = weightSlider.DOM
.map(vnode => {
vnode.sel += '.weight';
return vnode;
});
// ...
const heightVDom$ = heightSlider.DOM
.map(vnode => {
vnode.sel += '.height';
return vnode;
});
// ...
}
```
To avoid repeating code, such as the `.map(vnode => ...)` which patches the VNode, we could extract the functionality into functions: `isolateDOMSink()` and `isolateDOMSource()`.
```diff
function main(sources) {
// ...
const weightSources = {
- DOM: sources.DOM.select('.weight'), props: weightProps$
+ DOM: isolateDOMSource(sources.DOM, 'weight'), props: weightProps$
};
const heightSources = {
- DOM: sources.DOM.select('.height'), props: heightProps$
+ DOM: isolateDOMSource(sources.DOM, 'height'), props: heightProps$
};
// ...
- const weightVDom$ = weightSlider.DOM
- .map(vnode => {
- vnode.sel += '.weight';
- return vnode;
- });
+ const weightVDom$ = isolateDOMSink(weightSlider.DOM, 'weight');
// ...
- const heightVDom$ = heightSlider.DOM
- .map(vnode => {
- vnode.sel += '.height';
- return vnode;
- });
+ const heightVDom$ = isolateDOMSink(heightSlider.DOM, 'height');
// ...
}
```
Since these are very useful helper functions, they are packaged in Cycle DOM. They are available as static functions under the DOM source: `sources.DOM.isolateSource` and `sources.DOM.isolateSink`. This is how the `main()` function looks like when we use those functions:
```javascript
function main(sources) {
const weightProps$ = xs.of({
label: 'Weight', unit: 'kg', min: 40, value: 70, max: 150
});
const heightProps$ = xs.of({
label: 'Height', unit: 'cm', min: 140, value: 170, max: 210
});
const {isolateSource, isolateSink} = sources.DOM;
const weightSources = {
DOM: isolateSource(sources.DOM, 'weight'), props: weightProps$
};
const heightSources = {
DOM: isolateSource(sources.DOM, 'height'), props: heightProps$
};
const weightSlider = LabeledSlider(weightSources);
const heightSlider = LabeledSlider(heightSources);
const weightVDom$ = isolateSink(weightSlider.DOM, 'weight');
const weightValue$ = weightSlider.value;
const heightVDom$ = isolateSink(heightSlider.DOM, 'height');
const heightValue$ = heightSlider.value;
// ...
}
```
The code above shows how we need to manually process the sources and sinks of a child component to make sure each child is run in an isolated context. It would be better, however, if we could just "isolate" a component and make source and sink isolation happen under the hood.
Such is the purpose of [`isolate()`](https://github.com/cyclejs/cyclejs/tree/master/isolate) (`npm install @cycle/isolate`), a helper function which handles calls to `isolateSource` and `isolateSink` for us. `isolate(Component, scope)` takes a dataflow component function `Component` as input, and outputs a dataflow component function which isolates the sources to `scope`, runs `Component`, then isolates its sinks to `scope` as well. Here is a heavily simplified implementation of `isolate()`:
```javascript
function isolate(Component, scope) {
return function IsolatedComponent(sources) {
const {isolateSource, isolateSink} = sources.DOM;
const isolatedDOMSource = isolateSource(sources.DOM, scope);
const sinks = Component({DOM: isolatedDOMSource});
const isolatedDOMSink = isolateSink(sinks.DOM, scope);
return {
DOM: isolatedDOMSink
};
};
}
```
This allows us to simplify the `main()` function with two labeled slider components:
```diff
function main(sources) {
const weightProps$ = xs.of({
label: 'Weight', unit: 'kg', min: 40, value: 70, max: 150
});
const heightProps$ = xs.of({
label: 'Height', unit: 'cm', min: 140, value: 170, max: 210
});
- const {isolateSource, isolateSink} = sources.DOM;
const weightSources = {
- DOM: isolateSource(sources.DOM, 'weight'), props: weightProps$
+ DOM: sources.DOM, props: weightProps$
};
const heightSources = {
- DOM: isolateSource(sources.DOM, 'height'), props: heightProps$
+ DOM: sources.DOM, props: heightProps$
};
+ const WeightSlider = isolate(LabeledSlider, 'weight');
+ const HeightSlider = isolate(LabeledSlider, 'height');
- const weightSlider = LabeledSlider(weightSources);
+ const weightSlider = WeightSlider(weightSources);
- const heightSlider = LabeledSlider(heightSources);
+ const heightSlider = HeightSlider(heightSources);
- const weightVDom$ = isolateSink(weightSlider.DOM, 'weight');
+ const weightVDom$ = weightSlider.DOM;
const weightValue$ = weightSlider.value;
- const heightVDom$ = isolateSink(heightSlider.DOM, 'height');
+ const heightVDom$ = heightSlider.DOM;
const heightValue$ = heightSlider.value;
// ...
}
```
Notice the line which creates the `WeightSlider` component:
```javascript
const WeightSlider = isolate(LabeledSlider, 'weight');
```
`isolate()` takes a non-isolated component `LabeledSlider` and restricts it to the `'weight'` scope, creating `WeightSlider`. The scope `'weight'` is only used in this line of code, and nowhere else. We can simplify this code a bit more, by making the scope parameter implicit:
```javascript
const WeightSlider = isolate(LabeledSlider);
```
This does the same as previously, except the scope parameter is unique and autogenerated. The scope string itself was irrelevant to us, so we let `isolate()` generate some scope string for us.
> ### Is `isolate()` pure?
>
> If we leave the scope parameter implicit for both weight and height sliders, then the code becomes
>
> `const WeightSlider = isolate(LabeledSlider);`<br />
> `const HeightSlider = isolate(LabeledSlider);`
>
> Because the right-hand side is the same, does this mean `WeightSlider` and `HeightSlider` are the same component? **Certainly not.**
>
> `isolate()` with an implicit scope parameter is **not** referentially transparent. In other words, calling `isolate()` with an implicit scope is "impure". `WeightSlider` and `HeightSlider` are not the same components. Each one has its own unique scope parameter.
>
> On the other hand, when using an explicit scope parameter, then `isolate()` is referentially transparent. In other words, `Foo` and `Fuu` are the same here:
>
> `const Foo = isolate(LabeledSlider, 'myScope');`<br />
> `const Fuu = isolate(LabeledSlider, 'myScope');`
>
> Since Cycle.js follows functional programming techniques, usually most of its API is referentially transparent. `isolate()` is an exception, for convenience. If you want referential transparency everwhere, then provide explicit scope parameters. If you want convenience and you know how `isolate()` works, then use implicit scope parameters.
If we compare our last code with the code we initially started out naïvely for `main()` to make the BMI calculator, the only difference is the use of `isolate()` on child components:
```diff
function main(sources) {
const weightProps$ = xs.of({
label: 'Weight', unit: 'kg', min: 40, value: 70, max: 150
});
const heightProps$ = xs.of({
label: 'Height', unit: 'cm', min: 140, value: 170, max: 210
});
const weightSources = {DOM: sources.DOM, props: weightProps$};
const heightSources = {DOM: sources.DOM, props: heightProps$};
- const weightSlider = LabeledSlider(weightSources);
+ const weightSlider = isolate(LabeledSlider)(weightSources);
- const heightSlider = LabeledSlider(heightSources);
+ const heightSlider = isolate(LabeledSlider)(heightSources);
const weightVDom$ = weightSlider.DOM;
const weightValue$ = weightSlider.value;
const heightVDom$ = heightSlider.DOM;
const heightValue$ = heightSlider.value;
const bmi$ = xs.combine(weightValue$, heightValue$)
.map(([weight, height]) => {
const heightMeters = height * 0.01;
const bmi = Math.round(weight / (heightMeters * heightMeters));
return bmi;
})
.remember();
const vdom$ = xs.combine(bmi$, weightVDom$, heightVDom$)
.map(([bmi, weightVDom, heightVDom]) =>
div([
weightVDom,
heightVDom,
h2('BMI is ' + bmi)
])
);
return {
DOM: vdom$
};
}
```
The takeaway is: **when creating multiple instances of the same type of component, just remember to `isolate` each.**
<a class="jsbin-embed" href="//jsbin.com/seqehat/embed?output">JS Bin on jsbin.com</a>
## Recap
To achieve reusability, **any Cycle.js app is simply a function that can be reused as a component in larger Cycle.js app**. Sources and sinks are the interface between the application and the drivers, but they are also the interface between a child component and its parent.
![nested components](img/nested-components.svg)
From a component's perspective, it should make no assumption on what the parent is. The parent could either be the drivers if the component is used as the `main()`, or the parent could be any other component. For this reason, a component should assume its sources contain only data related to itself. Therefore, the sources and sinks of a component must be *isolated*.
Use `isolateSource` and `isolateSink` to separate the execution contexts of sibling components or unrelated components. Use `isolate` to create a component that automatically applies `isolateSource` and `isolateSink`. This way your codebase will be safe against [*collisions*](https://en.wikipedia.org/wiki/Collision_%28computer_science%29), and each component can work as if it would be the only one in the application.
Each driver should define static functions `isolateSource` and `isolateSink`. We only saw those functions implemented for the DOM Driver, but there are other use cases with other drivers where it makes sense to apply the same isolation techniques. To learn more, read about [Drivers](drivers.html).
</script>
<!-- Initializer -->
<script>
Flatdoc.run({
fetcher: function(callback) {
callback(null, document.getElementById('markdown').innerHTML);
},
highlight: function (code, value) {
return hljs.highlight(value, code).value;
},
});
</script>
</head>
<body role='flatdoc' class="no-literate">
<div class='header'>
<div class='left'>
<h1><a href="/"><img class="logo" src="img/cyclejs_logo.svg" >Cycle.js</a></h1>
<ul>
<li><a href='getting-started.html'>Documentation</a></li>
<li><a href='api/index.html'>API reference</a></li>
<li><a href='releases.html'>Releases</a></li>
<li><a href='https://github.com/cyclejs/cyclejs'>GitHub</a></li>
</ul>
<input id="docsearch" />
</div>
<div class='right'>
<!-- GitHub buttons: see https://ghbtns.com -->
<iframe src="https://ghbtns.com/github-btn.html?user=cyclejs&repo=cyclejs&type=watch&count=true" allowtransparency="true" frameborder="0" scrolling="0" width="110" height="20"></iframe>
</div>
</div>
<div class='content-root'>
<div class='menubar'>
<div class='menu section'>
<ul>
<li><a href="getting-started.html" class="level-1 out-link">起步</a></li>
<li><a href="dialogue.html" class="level-1 out-link">对话抽象</a></li>
<li><a href="streams.html" class="level-1 out-link">Streams</a></li>
<li><a href="basic-examples.html" class="level-1 out-link">Basic examples</a></li>
<li><a href="model-view-intent.html" class="level-1 out-link">Model-View-Intent</a></li>
</ul>
<div role='flatdoc-menu'></div>
<ul>
<li><a href="drivers.html" class="level-1 out-link">Drivers</a></li>
</ul>
</div>
</div>
<div role='flatdoc-content' class='content'></div>
</div>
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-101243593-1', 'auto');
ga('send', 'pageview');
</script>
<script>
((window.gitter = {}).chat = {}).options = {
room: 'cyclejs/cyclejs'
};
</script>
<script src="https://sidecar.gitter.im/dist/sidecar.v1.js" async defer></script>
<script src='//cdn.jsdelivr.net/docsearch.js/2/docsearch.min.js'></script>
<script src='docsearch.js'></script>
</body>
</html>