Table of Contents
- Setup
- Features
- Workflow
- Flow
- Example
- Writing tests
- Reducing the boilerplate and adding type safety
- Implementing sagas
- Router
- Recipes
- More examples
Setup
npm install tom --save
Features
- Elmish architecture
- Handles side effects in a declarative way
- Models, events and effects may be (static or runtime) type-checked
- Events are not required to be plain objects nor require a type field
Workflow
App configuration
A tom
app is defined by a config
object owning the following keys:
init. a function returning the initial state (a state is an object with a required key model
and an optional key effect
).
update. a update(model, event)
pure function, returns the new state.
view. a view(model, dispatch)
pure function, returns the ui declaration.
run (optional). a run(effect, event$)
function, returns an optional stream of events.
Wire them all
Call the start(config)
API.
Flow
Typings
(Flow syntax)
type IState<Model Effect> = model: Model; effect?: Effect;; type Dispatch<Event> = void; type IConfig<Model Effect Event View> = IState<Model Effect>; IState<Model Effect>; View; run?: ?Observable<Event>;}; type IApp<Event View> = dispatch: Dispatch<Event>; event$: Subject<Event> state$: Observable<IState> model$: Observable<Model> view$: Observable<View> effect$: Observable<Effect> nextEvent$$: Observable<Observable<Event>> nextEvent$: Observable<Event>; start<Model Effect Event View>config: IConfig<Model Effect Event View>: IApp<Event View>
Example
A delayed counter. When the buttons are pressed the counter is updated after 1 sec.
const config = { return model: 0 } { } { const increment = const decrement = return <div> <p>Counter: model</p> <button onClick=increment>+1</button> <button onClick=decrement>-1</button> </div> } // runs the side effects { } // start appconst view$ = // renderview$
Writing tests
You can easily test every part of you app:
// testing events // testing effects
Reducing the boilerplate and adding type safety
When your app grows you will face several issues:
update
,view
andrun
will become giant functions- using
switch
s inupdate
andrun
violates the open close principle - events and effects are not typed ("string programming"). The usual solution is to define constants and action creators (even more boilerplate)
- state is not type safe:
model
is actually an integer and this invariant should be enforced
To address the first 2 issues let's replace the strings with constructors and get rid of switch
s leveraging a kind of dynamic dispatch:
// events { return model effect: } { return model: model + 1 } { return model effect: } { return model: model - 1 } // effects { return RxObservable } { return RxObservable } const framework = { return event } { return effect } const config = { return model: 0 } { const increment = const decrement = return <div> <p>Counter: model</p> <button onClick=increment>+1</button> <button onClick=decrement>-1</button> </div> } Object
The update
and run
functions can now be reutilized across your apps.
Adding type safety
Here I'll use tcomb to add runtime type checking to a simple counter (alternatively you can use other tools like TypeScript or Flow, see the "More examples" section below):
// eventsconst Increment = tIncrementprototype { return model: model + 1 } const Decrement = tDecrementprototype { return model: model - 05 // this will throw "[tcomb] Invalid value -0.5 supplied to State/model: Integer"} const Event = t // stateconst Integer = tconst State = t const config = { return } { // type checking return } { const increment = const decrement = return <div> <p>Counter: model</p> <button onClick=increment>+1</button> <button onClick=decrement>-1</button> </div> }
Implementing sagas
(Example stolen from https://github.com/salsita/redux-saga-rxjs)
Let's imagine you want to withdraw money from ATM, the first thing you need to do is enter your credit card and then enter the PIN. So the sequence of transitions could be as follows:
WAITING_FOR_CREDIT_CARD
->CARD_INSERTED
->AUTHORIZED
orREJECTED
but we would like to allow user enter invalid PIN 3 times before rejecting
const VALID_PIN = '123'const PIN_VALIDATED = type: 'PIN_VALIDATED' const INVALID_PIN = type: 'INVALID_PIN' const PIN_REJECTED = type: 'PIN_REJECTED' Component { thisprops } { const model = thisprops const canIEnterPin = !modelauthFailure && !modelauthorized return <div> canIEnterPin && <div> <input ref="pin" /> <button disabled=modelisValidating onClick=thisonEnter>pin</button> </div> <p>modelerror && 'Invalid pin'</p> <p>modelauthorized && 'Authorized :)'</p> <p>modelauthFailure && 'Unauthorized :('</p> </div> } { return model: {} } { } { const onEnter = return <ATM model=model onEnter=onEnter /> } { }
Router
This library comes with a basic router that plays well with view streams.
Typings
type History = ...created with the history package...; type Location = pathname: string query: Object type Request<Context> = context?: Context history: History params: Object path: string pathname: string query: Object; type Handler<Context View> = View; type Route<Context View> = path: string handler: Handler<Context View>; interface Router<Context View> newroutes: Array<Route<Context View>> history: History; ; View;
Example
const history = /*{ queryKey: false }*/ const router =
Recipes
state
how to get the corresponding view stream
Given a const view$ =
How to know when a stable equilibrium is reached
const nextEvent$$ = let pending = appnextEvent$$
Monitoring
Monitoring an app is easy, just wrap the app with an helper function:
{ console console if model !== statemodel console else console console console} { if nextEvent$ // group produced events console nextEvent$ else console } { return { const init = config console return init } { const state = config return state } view: configview { const nextEvent$ = config return nextEvent$ } }
More examples
- A simple counter
- How to handle effects (delayed counter)
- How to reduce the boilerplate (dynamic dispatching)
- How to cancel effects (cancelable delayed counter)
- Perpetual effects (clock)
- Http requests
- Routing
- Saga pattern (Withdraw saga)
- How to handle optmistic updates (optmistic counter)
- How to test events and effects