ivan-kleshnin/cyclejs-examples
{ "createdAt": "2015-03-16T12:33:23Z", "defaultBranch": "master", "description": "CycleJS examples", "fullName": "ivan-kleshnin/cyclejs-examples", "homepage": "", "language": "JavaScript", "name": "cyclejs-examples", "pushedAt": "2017-11-14T12:01:04Z", "stargazersCount": 121, "topics": [], "updatedAt": "2023-08-28T05:21:20Z", "url": "https://github.com/ivan-kleshnin/cyclejs-examples"}cyclejs-examples
Section titled “cyclejs-examples”Example web applications built with Cycle.js.# CycleJS examples
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.
Install
Section titled “Install”- 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.).
Table of Contents
Section titled “Table of Contents”1.0-form
Section titled “1.0-form”Basic registration form.
1.1-form
Section titled “1.1-form”State. Dataflow.
1.2-form
Section titled “1.2-form”Actions. Update loop.
1.3-form
Section titled “1.3-form”Refactoring. Lenses.
1.4-form
Section titled “1.4-form”From models to types (implicit validation).
1.5-form
Section titled “1.5-form”Implement (explicit) validation.
2.0-routing
Section titled “2.0-routing”Minimal working example. Router, pages, menu, not-found.
2.1-routing
Section titled “2.1-routing”Refactoring. Highlight “current” menu item.
2.2-routing
Section titled “2.2-routing”Use route-parser library.
Models and URL params.
2.3-routing
Section titled “2.3-routing”Implement link-based nested menus.
3.0-crud
Section titled “3.0-crud”Basic CRUD + Index example. Types, forms, validation, navigation, and state management at once.
20.0-memory-game
Section titled “20.0-memory-game”20.1-tetris-game
Section titled “20.1-tetris-game”Glitches
Section titled “Glitches”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.
No trailing $
Section titled “No trailing $”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 whyDOMhas 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 :: Userusers - array of models :: [User]user$ - model stream :: Observable Userusers$ - 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")).
Console driver
Section titled “Console driver”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: ... }}