re-frame module

mreframe: A reagent/re-frame imitation that uses Mithril instead


re-frame module

Based on ClojureScript re-frame library; used for managing state, events and side-effects in a reagent application.

Suggested imported module name is rf.

The general usage is as follows:

  • a view (component) can dispatch events (rf.dispatch()/rf.dispatchSync()), and subscribe to queries (rf.subscribe())
  • an event handler is a pure function which receives event query and current state (computed by coeffects), and returns a set of effects to be actioned later (dictionary of key-value pairs, each determining an effect and its argument)
  • a coeffect is a value obtained via impure calculation (like current time or a localStorage value)
  • an effect is a function which produces side-effects (like sending AJAX requests or updating localStorage)
  • an interceptor is a modifier which is added to event processing chain to alter its coeffects or effects
  • interceptors have 2 methods (before & after); before is called when processing the chain upwards to the event handler, after is called when the event handler had been run and chain is being processed downwards

Aside from implementing most of re-frame API, this module exports 2 helper functions: dsub (deref+subscribe), and disp (dispatch wrapper to be used in effects, includes event existence check & allows for supplying additional parameters).

Includes 4 built-in effects (see rf.regEventFx): db, fx, dispatch, and dispatchLater.

_init (opts)

A setup function (only necessary if you’re using nodeps bundle); opts may include:

  • eq: deep equality comparison (for db update checks; defaults to util.eq);
  • additional opts for r._init().

This is the _init exported in the main mreframe module. It also invokes r._init(), which is where most opts are actually used.

To replace db update checks with shallow-equality, run rf._init({eq: eqShallow}), and to use identity checks instead, run rf._init({eq: identical}) (see util).

dispatch (event)

Asynchronously dispatches an event (described as ['id', ...args]).

rf.dispatch(['set', 'key', value]) // dispatches event ['set', 'key', value] after a tick

dispatchSync (event)

Dispatches an event immediately (use for synchronous updates, like oninput of a text fields with supplied value, or for state initialization).

rf.dispatchSync(['set', 'key', value]) // *immediately* dispatches the event
rf.dispatchSync(['init-db', initialState])

regEventDb (id, [interceptors], handler)

Assigns handler to event id, with optional interceptors added to the processing chain; handler accepts (db, event) as arguments and returns new db.

rf.regEventDb('set', (db, [_, key, value]) => assoc(db, key, value))
rf.regEventDb('set-in', [trimV], (db, [path, value]) => // removed event id from the query
ssocIn(db, path, value))

regEventFx (id, [interceptors], handler)

Same, but handler accepts (coeffects, event) as arguments (db is accessible as coffects.db) and returns effects (dict of key-value pairs describing effects to be actioned; the one updating app-db is called 'db').

rf.regEventFx('fetch-data', ({db}, [_, url, key]) =>
  ({db:    assoc(db, 'lastUrl', url),
    fetch: {url, onSuccess: ['set', key]}}))
rf.regEventFx('show-time', [rf.injectCofx('now')], ({time}, _) => // 'time' supplied by custom coeffect
  ({alert: `Current time is ${time}`}))

regEventCtx (id, [interceptors], handler)

Same, but handler accepts (context) as argument.

rf.regEventCtx('debug', context => ({...context, effects: {log: context}}))  // logs context value
rf.regEventCtx('walk-back', [injectCofx(foo), injectCofx(bar)], context =>
  ({...context, stack: context.stack.slice().reverse()})) // reverse interceptors list on walk back

clearEvent (id?)

Removes a single event by id, or all events if no id was provided.

rf.clearEvent('debug')  // remove event at runtime
rf.clearEvent()         // remove all events

regSub (id, computation)

Registers a subscription; computation accepts (db, query) as arguments and returns its result.

rf.regSub('lists', getIn) // supports queries ['lists'], ['lists', id], ['lists', id, 5]…

regSub (id, signals, computation)

Registers a subscription; computation accepts (inputs, query) as arguments, with inputs depending on result of calling signals(query):

  • deref( signals() ) if signals is a function regurning an atom
  • signals().map(deref) if signals is a function returning a list of atoms
  • dict( entries( signals() ).map(([k, v]) => [k, deref(v)]) ) if signals is a function returning a dictionary of atoms
    rf.regSub('count', ([_, id] => rf.subscribe(['lists', id])), (list, _) => list.length)
    rf.regSub('join', ([_, ...ids] => ids.map(id => rf.subscribe(['lists', id]))), (lists, _) =>
    [].concat(...lists))
    rf.regSub('subset',
            ([_, foo, bar] => {[foo]: rf.subscribe(['lists', foo]), [bar]: rf.subscribe(['lists', bar])}),
            (o, [_, foo, bar]) => ({foo: o[foo], bar: o[bar]}))
    

regSub (id, ...signals, computation)

Same, but signals function is presented as a list of subscription shorthands
('<-', ['foo', 1], '<-', ['bar', 2] is equivalent to _ => [rf.subscribe(['foo', 1]), rf.subscribe(['bar', 2])]);
in case of one subscription it’s a single-value instead ('<-', ['foo', 1]_ => rf.subscribe(['foo', 1])).

rf.regSub('total-length', '<-', ['lists'], (lists, _) =>
  Object.values(lists).map(xs => xs.length).reduce((n, m) => n+m, 0))
rf.regSub('foo&bar',
          '<-', ['lists', 'foo'],
          '<-', ['lists', 'bar'],
          ([foo, bar], _) => [].concat(foo, bar))

subscribe (query)

Returns an atom which derefs to the current value of subscription defined by query (with the result of computation being cached by its signals for each query); abstain from passing non-plain data here! (only scalars, strings, arrays, dicts, and RegExps are supported).

rf.subscribe(['lists', 'foo'])  // cursor that queries 'lists' with parameter 'foo'

clearSub (id?)

Removes a single subscription by id, or all subscriptions if no id was provided.

rf.clearSub('total-length') // remove subscription at runtime
rf.clearSub()               // remove all subscriptions

clearSubscriptionCache ()

Removes all subscriptions from the cache (useful for development/debugging).

regFx (id, handler)

Registers an effect by id; handler accepts one argument passed by the event that triggers it (so foo: 1 triggers effect foo with argument 1).

rf.regFx('fetch', ({url, onSuccess, onFailure}) =>
  fetch(url).then(x => x.text()).then(s => {onSuccess && rf.dispatch([...onSuccess, s])})
            .catch(error => {onFailure && rf.dispatch([...onFailure, error])}))

clearFx (id?)

Removes a single effect by id, or all effects if no id was provided.

rf.clearFx('fetch') // remove effect at runtime
rf.clearFx()        // remove all effects

toInterceptor ({id, before, after})

Produces an interceptor with before and after methods (id is decorative).

let fxLogger = rf.toInterceptor({
  id: 'fx-logger',
  after: context => (console.warn( rf.getEffect(context) ), context),
})
// ⇒ interceptor structure which prints out effects on backtracking (for debug purposes)
rf.regEventFx('some-event', [fxLogger], someFxEvent)

regCofx (id, handler)

Registers a coeffect by id; handler accepts (coeffects, arg?) and returns new coeffects dictionary.

rf.regCofx('now', cofx => assoc(cofx, key, new Date())) // see rf.regEventFx example
rf.regCofx('load', (cofx, key) =>
  assoc(cofx, key, JSON.parse(localStorage.getItem(key) || "null")))

injectCofx (id, arg?)

Returns an interceptor calling id coeffect in its before call.

rf.injectCofx('now')          // ⇒ interceptor that supplies current time as coeffects.time
rf.injectCofx('load', 'foo')  // ⇒ interceptor that supplies localStorage.foo as coeffects.foo
rf.regEventFx('reset-foo', [rf.injectCofx('load', 'foo')], ({db, foo}, _) =>
  ({db: merge(db, {foo})}))

clearCofx (id?)

Removes a single coeffect by id, or all coeffects if no id was provided.

rf.clearCofx('now') // remove coeffect at runtime
rf.clearCofx()      // remove all coeffects

path (...path)

Produces an interceptor which substitudes db with its path in the context with its before method, and replaces subvalue of db in path with db value in context (from effects or coeffects); path may contain nested lists.

rf.regEventDb('inc-5th-foo', [rf.path('lists', 'foo', 5)], n => n+1)
// equivalent to db => assocIn(db, ['lists', 'foo', 5], 1 + getIn(db, ['lists', 'foo', 5]))
const urls = ['config', 'urls']
let fetchFxEvent = (url, [_, key]) => ({fetch: {url, onSuccess: ['set', key]}})
urlIds.forEach(id => rf.regEventFx(`fetch-${id}`, [rf.path(urls, id)], fetchFxEvent)
// equivalent to {fetch: {url: getIn(db, [...urls, id]), onSuccess: ['set', key]}}

enrich (f)

Produces an interceptor which applies f to db and event in its after method and replaces effects.db with it.

let fallbackStr = rf.enrich((db, [_, key]) => assoc(db, key, db[key] || ""))
rf.regEventDf('set-string', [fallbackStr], (db, [_, key, value]) => assoc(db, key, value))
// ensure that a string value db[key] is never changed to nil

unwrap

An interceptor which replaces event in context with its first parameter (original event is set in coeffects.originalEvent).

rf.regEventDb('inc-counter', [rf.unwrap], (db, key) => update(db, key, n => n+1))
rf.dispatch(['inc-counter', 'foo'])

trimV

An interceptor which replaces event in context with its parameters (original event is set in coeffects.originalEvent).

rf.regEventFx('log-value', [rf.trimV], (db, path) => ({log: getIn(db, path)}))
rf.dispatch(['log-value', 'lists', 'foo', 5]) // prints out db.lists.foo[5]
rf.dispatch(['log-value'])                    // prints out db

after (f)

Produces an interceptor which applies a side-effect to db.

rf.regEventDb('add-list', [rf.trimV, rf.path('lists'), rf.after(console.warn)],
              (lists, [k, v]) => assoc(lists, k, v))
// an event which alters db.lists then prints it out

onChanges (f, outPath, ...inPaths)

Produces an interceptor which updates outPath if any of the values on inPaths got changed, by calling f() on these values.

let concat = (...xss) => [].concat(...xss)
rf.regEventDb('set-list',
              [rf.trimV, rf.path('lists'), rf.onChanges(concat, ['fooBar'], ['foo'], ['bar'])],
              (lists, [k, v]) => assoc(lists, k, v))
// an event which sets a value in db.lists, and recalculates .fooBar if .foo or .bar change

getCoeffect (context, key?, notFound?)

Returns coeffects from event context (or one of coeffects by key, or notFound if it wasn’t in there).

rf.getCoeffect(context)             // ⇒ coeffects
rf.getCoeffect(context, 'foo')      // ⇒ coeffects.foo
rf.getCoeffect(context, 'foo', 42)  // ⇒ coeffects.foo or 42 (if .foo was not found)

assocCoeffect (context, key, value)

Returns updated context where key field in coeffects is set to value

rf.assocCoeffect(context, 'foo', 42)  // ⇒ updated context, coeffects.foo = 42

getEffect (context, key?, notFound?)

Returns effects from event context (or one of effects by key, or notFound if it wasn’t in there).

rf.getEffect(context)                                       // ⇒ effects
rf.getEffect(context, 'db')                                 // ⇒ effects.db
rf.getEffect(context, 'db', rf.getCoeffect(context, 'db'))  // ⇒ effects.db or coeffects.db

assocEffect (context, key, value)

Returns updated context where key field in effects is set to value

rf.assocEffect(context, 'db', rf.getCoeffect(context, 'db'))  // reset changes to db

enqueue (context, interceptors)

Adds more interceptors to the end of the interceptors queue.

rf.enqueue(context, [rf.path( rf.getCoeffect('event')[1] )])
// dynamically add rf.path from event parameter

purgeEventQueue ()

Cancels all scheduled events.

dsub (query)

An alias to deref( subscribe(query) ).

rf.dsub(['lists', 'foo']) // same as deref( rf.subscribe(['lists', 'foo']) )

disp (event, ...args)

A helper for dispatching partial events.

rf.regFx('fetch', ({url, onSuccess, onFailure}) =>
  fetch(url).then(x => x.text()).then(s => rf.disp(onSuccess, s))
            .catch(error => rf.disp(onFailure, error)))

'db' builtin effect

Resets app-db to its argument.

{db: assoc(db, 'answer', 42)}

'fx' builtin effect

Actions effects in the given order (e.g. fx: [['foo', 1], ['bar', 2]] invokes foo: 1 then bar: 2); also allows for actioning the same effect multiple times, or to action effects conditionally.

{fx: [['db', assoc(db, 'answer', 42)],
      (db.answer !== 42) &&
        ['alert', "State was updated!"]]}
{fx: [['fetch', {url: "/api/foo", onSuccess: ['setFoo']}],
      ['fetch', {url: "/api/bar", onSuccess: ['setBar']}]]}

'dispatchLater' builtin effect

Dispatches an event after a delay (see rf.dispatch()); expects a dict of {ms, dispatch} (where ms is delay time, and dispatch is the dispatched event).

{dispatchLater: {dispatch: ['setTitle', "Ready!"], ms: 5000}}
{dispatchLater: {dispatch: ['setAnswer', 42]}}

'dispatch' builtin effect

Shorthand for dispatchLater with no ms argument. Conveniend to use in fx.

{dispatch: ['setAnswer', 42]}
{fx: [['dispatch', ['foo']],
      ['dispatch', ['bar']]]}