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
localStoragevalue) - 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);beforeis called when processing the chain upwards to the event handler,afteris 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 toutil.eq);- additional
optsforr._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).
inNamespace (namespace)
Produces an isolated (“namespaced”) instance of re-frame module, with a separate appDb, subscriptions, events, effects,
and coeffects. Invoking twice with the same namespace produces the same instance (base instance namespace is '').
You can identify a re-frame module instance by its namespace property.
let _rf = rf.inNamespace('progress-frame');
console.debug(_rf.namespace); // logs "progress-frame"
_rf.regEventDb('init-db', [unwrap], (_, db) => db); // generic event names etc won't cause a collision with base module
(Note: _init() is meant to be used once, applies to all namespaces, and is exposed only by the base module instance.)
This feature is useful when implementing standalone widgets that work independently of each other; e.g. in a userscript.
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() )ifsignalsis a function regurning an atomsignals().map(deref)ifsignalsis a function returning a list of atomsdict( entries( signals() ).map(([k, v]) => [k, deref(v)]) )ifsignalsis a function returning a dictionary of atomsrf.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']]]}