Skip to content
This repository was archived by the owner on Dec 18, 2019. It is now read-only.

Commit bfcb5f3

Browse files
committed
Update README.md for new makeCollection API
1 parent 3e875c9 commit bfcb5f3

File tree

1 file changed

+52
-80
lines changed

1 file changed

+52
-80
lines changed

README.md

Lines changed: 52 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@ Augments your [Cycle.js](https://cycle.js.org) main function with onion-shaped s
44

55
- **Simple:** all state lives in one place only
66
- **Predictable:** use the same pattern to build any component
7-
- **Reusable:** you can move any component to any other codebase using onionify
8-
- **Tiny:** library has less than 100 lines of code, less than 2 kB
7+
- **Reusable:** you can move any onionify-based component to any other Cycle.js codebase
98

109
Quick example:
1110

@@ -73,8 +72,6 @@ As a consequence, state management is layered like an onion. State streams (sour
7372

7473
**Fractal, like most Cycle.js apps should be.** Unlike Redux, there is no *global entity* in the onion state architecture, except for the usage of the `onionify` function itself, which is one line of code. The onion state architecture is similar to the Elm architecture in this regard, where any component is written in the same way without expecting any global entity to exist. As a result, you gain reusability: you can take any component and run it anywhere else because it's not tied to any global entity. You can reuse the component in another Cycle.js onionified app, or you can run the component in isolation for testing purposes without having to mock any external dependency for state management (such as a Flux Dispatcher).
7574

76-
**Does not require IDs to manage a list of components.** In Redux, Elm arch, traditional Cycle.js, and [Stanga](https://github.com/milankinen/stanga), to manage a collection of components (such as a dynamic to-do list), you need to issue unique IDs and manage them when reducers need to update a particular entry. In [Redux TodoMVC](https://github.com/reactjs/redux/blob/055f0058931465af6f96213a99461fc852f83c61/examples/todomvc/src/reducers/todos.js#L16), this is `id: state.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1,` and in [Elm TodoMVC](https://github.com/evancz/elm-todomvc/blob/11fc6bef5f53ebd062caf9b6ce7b95e84634e9a5/Todo.elm#L129), this is `{ model | uid = model.uid + 1}`. In onionify, to manage arrays in the state tree you do not need ID numbers at all. For instance, a to-do entry component can delete itself from the list using a reducer that returns undefined as the new state. See the source code for [TodoMVC Cycle.js onionified](https://github.com/cyclejs/todomvc-cycle/tree/onionify/src).
77-
7875
# How to use onionify
7976

8077
### How to set up
@@ -83,8 +80,6 @@ As a consequence, state management is layered like an onion. State streams (sour
8380
npm install --save cycle-onionify
8481
```
8582

86-
[xstream](http://staltz.com/xstream/) v10 is a hard dependency. If you use npm v3 (you should), this should not create multiple instances of xstream in `node_modules`, since most Cycle.js packages are using xstream v10 too.
87-
8883
Import and call onionify on your `main` function (the top-most component in your app):
8984

9085
```js
@@ -241,60 +236,51 @@ function Parent(sources) {
241236
}
242237
```
243238

244-
Each object `{ count: i }` in the array can become the state object for a child component. We need to call the `Child` component with isolate, and passing a unique isolation *scope* for each. To do that, we can just use the indices of the array items.
245-
246-
```js
247-
function Parent(sources) {
248-
const array$ = sources.onion.state$;
249-
250-
const childrenSinks$ = array$.map(array =>
251-
array.map((item, index) => isolate(Child, index)(sources))
252-
);
253-
254-
// ...
255-
}
256-
```
257-
258-
As you see, instead of a `childSinks`, we get a `childrenSinks$`, which is a stream that emits **arrays with sinks**. Each item in that array is a sinks object for each child instance. Item 0 is the sinks object for the first child, item 1 is the sinks object for the second child, and so forth. And as a reminder, each sinks object has multiple streams. So it's a *stream of arrays of objects with streams*.
239+
Each object `{ count: i }` in the array can become the state object for a child component. Onionify comes with a helper function called `makeCollection` which will utilize the array state stream to infer which children instances should be created, updated, or removed.
259240

260-
This may seem like a complex structure, but all you need to know is that `childrenSinks$` contains all sinks for all children components. Onionify provides helpers to easily handle them: custom xstream operators `pick` and `mix`.
241+
`makeCollection` takes a couple of options and returns a normal Cycle.js component (function from sources to sinks). You should specify the child component, a unique identifier for each array element (optional), an isolation scope (optional), and how to combine all children sinks together.
261242

262243
```js
263-
import {pick, mix} from 'cycle-onionify';
244+
const List = makeCollection({
245+
item: Child,
246+
itemKey: (childState, index) => String(index), // or, e.g., childState.key
247+
itemScope: key => key, // use `key` string as the isolation scope
248+
collectSinks: instances => {
249+
return {
250+
onion: instances.pickMerge('onion'),
251+
// ...
252+
}
253+
}
254+
})
264255
```
265256

266-
Suppose you want to get all reducers from all children and merge them together. First you "pick" the `onion` sink from each child sink (this is similar to lodash [get](https://lodash.com/docs/4.16.4#get) or [pick](https://lodash.com/docs/4.16.4#get)), then the outcome will be a stream of arrays, where array items are reducer streams. Second, you merge all those onion sinks together with `mix(xs.merge)`, to get a simple stream of reducers.
267-
268-
```js
269-
const childrenSinks$ = array$.map(array =>
270-
array.map((item, index) => isolate(Child, index)(sources))
271-
);
272-
273-
const childrenReducers$ = childrenSinks$
274-
.compose(pick(sinks => sinks.onion)); // or...
275-
//.compose(pick('onion'));
276-
// it does the same thing
257+
In `collectSinks`, we are given an `instances` object, it is an object that represents all sinks for all children components, and has two helpers to handle them: `pickMerge` and `pickCombine`. These work like the xstream operators `merge` and `combine`, respectively, but operate on a dynamic (growing or shrinking) collection of children instances.
277258

278-
const childrenReducer$ = childrenReducers$
279-
.compose(mix(xs.merge));
280-
```
259+
Suppose you want to get all reducers from all children and merge them together. You use `pickMerge` that first "picks" the `onion` sink from each child sink (this is similar to lodash [get](https://lodash.com/docs/4.16.4#get) or [pick](https://lodash.com/docs/4.16.4#get)), and then merges all those onion sinks together, so the output is a simple stream of reducers.
281260

282-
Then, you can merge the children reducers with the parent reducers (if there are any), and return those from the parent:
261+
Then, you can merge the children reducers (`listSinks.onion`) with the parent reducers (if there are any), and return those from the parent:
283262

284263
```js
285264
function Parent(sources) {
286265
const array$ = sources.onion.state$;
287266

288-
const childrenSinks$ = array$.map(array =>
289-
array.map((item, index) => isolate(Child, index)(sources))
290-
);
267+
const List = makeCollection({
268+
item: Child,
269+
itemKey: (childState, index) => String(index),
270+
itemScope: key => key,
271+
collectSinks: instances => {
272+
return {
273+
onion: instances.pickMerge('onion'),
274+
// ...
275+
}
276+
}
277+
});
291278

292-
const childrenReducers$ = childrenSinks$.compose(pick('onion'));
293-
const childrenReducer$ = childrenReducers$.compose(mix(xs.merge));
279+
const listSinks = List(sources);
294280

295281
// ...
296282

297-
const reducer$ = xs.merge(childrenReducer$, parentReducer$);
283+
const reducer$ = xs.merge(listSinks.onion, parentReducer$);
298284

299285
return {
300286
onion: reducer$,
@@ -303,13 +289,21 @@ function Parent(sources) {
303289
}
304290
```
305291

306-
This same pattern above should be used in most cases where you need to pick the onion sink from each child and merge them. However, `mix()` allows you to use not only `merge`, but also `combine`. This is useful when combining all children DOM sinks together as one array:
292+
As `pickMerge` is similar to `merge`, `pickCombine` is similar to `combine` and is useful when combining all children DOM sinks together as one array:
307293

308294
```js
309-
const vdom$ = childrenSinks$
310-
.compose(pick('DOM'))
311-
.compose(mix(xs.combine))
312-
.map(itemVNodes => ul(itemVNodes));
295+
const List = makeCollection({
296+
item: Child,
297+
itemKey: (childState, index) => String(index),
298+
itemScope: key => key,
299+
collectSinks: instances => {
300+
return {
301+
onion: instances.pickMerge('onion'),
302+
DOM: instances.pickCombine('DOM')
303+
.map(itemVNodes => ul(itemVNodes))
304+
}
305+
}
306+
});
313307
```
314308

315309
Depending on the type of sink, you may want to use the `merge` strategy or the `combine` strategy. Usually `merge` is used for reducers and `combine` for Virtual DOM streams. In the more general case, `merge` is for events and `combine` is for values-over-time (["signals"](https://github.com/cyclejs/cyclejs/wiki/Understanding-Signals-vs-Events)).
@@ -421,29 +415,15 @@ Cycle.run(wrappedMain, drivers);
421415

422416
### How to use it with TypeScript
423417

424-
We recommend that you export these types for every component: `Action`, `State`, `Reducer`, `Source`, `Sinks`. Below is an example of what these types usually look like:
418+
We recommend that you export the type `State` for every component. Below is an example of what this usually looks like:
425419

426420
```typescript
427-
export interface BleshAction {
428-
type: 'BLESH';
429-
payload: number;
430-
};
431-
432-
export interface BloshAction {
433-
type: 'BLOSH';
434-
payload: string;
435-
};
436-
437-
export type Action = BleshAction | BloshAction;
438-
439421
export interface State {
440422
count: number;
441423
age: number;
442424
title: string;
443425
}
444426

445-
export type Reducer = (prev?: State) => State | undefined;
446-
447427
export interface Sources {
448428
DOM: DOMSource;
449429
onion: StateSource<State>;
@@ -465,7 +445,7 @@ The `StateSource` type comes from onionify and you can import it as such:
465445
import {StateSource} from 'cycle-onionify';
466446
```
467447

468-
Then, you can compose nested state types:
448+
Then, you can compose nested state types in the parent component file:
469449

470450
```typescript
471451
import {State as ChildState} from './Child';
@@ -489,7 +469,7 @@ That said, state vs props management is too hard to master with Cycle.js (and al
489469

490470
### Does it support [RxJS](http://reactivex.io/rxjs/) or [most.js](https://github.com/cujojs/most)?
491471

492-
No, not yet. It only supports xstream. However, for the time being, you could try to implement onionify on your own, since it's just less than 100 lines of code. In the long run, we want to build [Cycle Unified](https://github.com/cyclejs/cyclejs/issues/425) which could allow running this tool with RxJS or most.js.
472+
Yes, as long as you are using Cycle.js and the *RxJS Run* package (or Most.js Run).
493473

494474
### Does it support [Immutable.js](https://facebook.github.io/immutable-js/)?
495475

@@ -520,38 +500,30 @@ function main(sources) {
520500
}
521501
```
522502

523-
### How does this work?
524-
525-
[Read the source](https://github.com/staltz/cycle-onionify/blob/master/src/index.ts). It's less than 150 lines of code, and probably quicker to read the source than to explain it in words.
526-
527503
### Why is this not official in Cycle.js?
528504

529505
If all goes well, eventually this will be an official Cycle.js practice. For now, we want to experiment in the open, collect feedback, and make sure that this is a solid pattern. There are [other approaches](https://github.com/cyclejs/cyclejs/issues/312) to state management in Cycle.js and we want to make sure the most popular one ends up being the official one.
530506

531-
### How does this compare to [Stanga](https://github.com/milankinen/stanga)?
532-
533-
- Stanga is a "driver". Onionify is a component wrapper function.
534-
- Stanga defines initial state as a separate argument. Onionify defines initial state as a reducer.
535-
- Stanga uses helper functions and lenses for sub-states. Onionify leverages `@cycle/isolate`.
536-
- Stanga uses unique IDs for managing dynamic lists. Onionify does not.
537-
538507
### How does this compare to [Redux](http://redux.js.org/)?
539508

540509
- Redux is not fractal (and has a visible global entity, the Store). Onionify is fractal (and has an invisible global entity).
541510
- Redux defines initial state in the argument for a reducer. Onionify defines initial state as a reducer itself.
542511
- Redux reducers have two arguments `(previousState, action) => newState`. Onionify reducers have one argument `(previousState) => newState` (the action is given from the closure).
543-
- Redux uses unique IDs for managing dynamic lists. Onionify does not.
512+
513+
### How does this compare to [Stanga](https://github.com/milankinen/stanga)?
514+
515+
- Stanga is a "driver". Onionify is a component wrapper function.
516+
- Stanga defines initial state as a separate argument. Onionify defines initial state as a reducer.
517+
- Stanga uses helper functions and lenses for sub-states. Onionify leverages `@cycle/isolate`.
544518

545519
### How does this compare to the [Elm architecture](https://guide.elm-lang.org/architecture/index.html)?
546520

547521
- Both are fractal.
548522
- Elm reducers have two arguments `Msg -> Model -> ( Model, Cmd Msg )`. Onionify reducers have one argument `(previousState) => newState`.
549523
- Elm child reducers are explicitly composed and nested in parent reducers. Onionify child reducers are isolated with `@cycle/isolate` and merged with parent reducers.
550524
- Elm child actions are nested in parent actions. In onionify, actions in child components are unrelated to parent actions.
551-
- Elm architecture uses unique IDs for managing dynamic lists. Onionify does not.
552525

553526
### How does this compare to ClojureScript [Om](https://github.com/omcljs/om)?
554527

555528
- Om [Cursors](https://github.com/omcljs/om/wiki/Cursors) are very similar in purpose to `onionify` + `isolate`.
556529
- Om cursors are updated with imperative `transact!`. Onionify state is updated with declarative reducer functions.
557-
- Om uses unique IDs for managing dynamic lists. Onionify does not.

0 commit comments

Comments
 (0)