This repo is frozen as I don't use CycleJS anymore. Check Unredux for more details.
Subjective followup to the official repo.
Examples are grouped into lessons and placed in narrative order.
They are meant to be reviewed one by one, sequentially.
The best way of learning is comparison. And to compare you just diff files.
- Download and unzip repo
- Go to unzipped folder
- Install packages with
$ npm install
- Run dev server with
$ npm run {example-number}
(only number, no suffix) - See
localhost:8080
We recommend to open index.html
with http://
(i.e. serve it as described above) because
many things in browser simply don't work for file://
(history, CORS, etc.).
Basic registration form.
State. Dataflow.
Actions. Update loop.
Refactoring. Lenses.
From models to types (implicit validation).
Implement (explicit) validation.
Minimal working example. Router, pages, menu, not-found.
Refactoring. Highlight "current" menu item.
Use route-parser library.
Models and URL params.
Implement link-based nested menus.
Basic CRUD + Index example. Types, forms, validation, navigation, and state management at once.
Diamond cases in stream topologies will cause unnecessary events called "glitches". RxJS does not apply topological sorting to suppress them (as Bacon or Flyd do). Performance and memory usage are gradually improved but not without consequences.
Imagine you have state
and derivedState
streams.
DOM depends from both state
and derivedState
.
let derivedState = state.map(...)
let DOM = Observable.combineLatest(state, derivedState, (state, derivedState) => /* render DOM */)
Now every time a change in state
will cause a change in derivedState
you'll have two DOM rendering instead of one.
There are basically three ways to address this:
-
Use
.withLatestFrom()
and / or.zip()
to express your dataflow as a set of control and data streams. May be surprisingly hard to implement and support. -
Debounce glitches. Derived states are mostly sync calculations so
debounce(1)
will work like a charm.
DOM: Observable.combineLatest(...whatever).debounce(1).map(([...]) => /* render DOM */)
- Tolerate glitches. May be a good choice while you're not confident about dataflow. As long as side effects are relative painless (DOM diffs are) – it's only a performance issue, man.
Convention of obs$
was used here previously but we've changed my mind since then.
Five reasons to discard it:
-
It's inconsistent inside CycleJS.
vtree$
vsDOM
– both are streams but named differently.
There is a strong reason whyDOM
has no$
(filename...) but it's still inconsistent. -
Related projects (RxJS, Elm, etc.) does not follow this convention.
-
It turns out to be harder to read. Nested streams look especially ugly:
Observable.merge(
intents.form.changeUsername.map(...),
intents.form.changeEmail.map(...),
intents.form.register.map(...)
)
// vs
Observable.merge(
intents.form.changeUsername$.map(...),
intents.form.changeEmail$.map(...),
intents.form.register$.map(...)
)
-
It fails to represent all the cases:
user - single model :: User users - array of models :: [User] user$ - model stream :: Observable User users$ - array stream :: Observable [User]
So far so good. Even
user$s - array of model streams :: [Observable User] users$s - array of array streams :: [Observable [User]]
kinda work. Until you hit a special word:
... peopl$e ? @_@
What about records of streams? Clear enough, I hope.
-
No confusion between static and observable variables were confirmed in practice.
Variables tend to be either first or second type in every particular (flat) namespace.Simple rule: do not mix static and observable keys in records.
Caution: you may hit troubles with "forbid shadowing" rule in IDE or linter.
So for now we're sticking with "repeat names" rule:
Observable.combineLatest(
foo, bar, // observable vars
(foo, bar) => { // static vars
...
}
)
and we use $
as a shortcut for statics in Observable
(let {Observable: $} = require("rx")
).
Is not a joke. It's really required in rare cases. If you try
function page1(src) {
...
intents.foo.subscribe((...) => {
console.log(...)
})
...
}
function page2(src) {
...
intents.foo.subscribe((...) => {
console.log(...)
})
...
}
you may get an impression that "architecture is broken": page events are repeating, interleaving, etc.
Which is wrong. Multipage architecture works because of flatMapLatest
which
disposes subscriptions no longer required. Subscription style shown above is unmanageable and leads
to memory leaks.
You should use tap
instead of subscribe
:
function page1(src) {
...
intents.foo.tap((...) => {
console.log(...)
})
...
}
function page2(src) {
...
intents.foo.tap((...) => {
console.log(...)
})
...
}
or you can utilize console driver:
function page1(src) {
...
return {
log: intents.foo.map(...) // convert intent value to string
DOM: ...
}
}
function page2(src) {
...
return {
log: intents.foo.map(...) // convert intent value to string
DOM: ...
}
}