mreframe
is a plain JavaScript re-implementation of reagent
and
re-frame
libraries from ClojureScript;
it’s a mini-framework for single-page apps (using Mithril as the base renderer, with some interop).
- Lightweight, both in size and use: just load a small JavaScript file,
require
it as a library, and you’re good to go - No language/stack requirement – you can use JS directly, or any language that transpiles into it as long as it has interop
- Simple, data-centric API using native JS data structures and plain functions for rendering, event handling, and querying state
- Components, events and queries have no need to expose their inner workings beyond the level of a simple function call
- Improved performance of re-rendering large, complex UI by preventing recalculation of unchanged subtrees
Install: npm i mreframe
/yarn add mreframe
or <script src="https://unpkg.com/mreframe/dist/mreframe.min.js"></script>
.
Here’s a full app code example:
let {reagent: r, reFrame: rf} = require('mreframe');
// registering events
rf.regEventDb('init', () => ({counter: 0})); // initial app state
rf.regEventDb('counter-add', (db, [_, n]) => ({...db, counter: db.counter + n}));
// registering state queries
rf.regSub('counter', db => db.counter);
// component functions
let IncButton = (n, caption) =>
['button', {onclick: () => rf.dispatch(['counter-add', n])}, // invoking counter-add event on button click
caption];
let Counter = () =>
['main',
['span.counter', rf.dsub(['counter'])], // accessing app state
" ",
[IncButton, +1, "increment"]];
// initializing the app
rf.dispatchSync(['init']); // app state needs to be initialized immediately, before first render
r.render([Counter], document.body);
Tutorial / live demo: Reagent (components), re-frame (events/state management).
Intro
ClojureScript has a very good functional interface to React (as third party libraries), allowing one to model DOM using data literals, to define components as plain functions (or functions returning functions), and to make best use of pure functions when defining calculations and decision-making logic.
Wisp is a lightweight Lisp variant based on ClojureScript; however, it’s harder to use for
SPAs as there’s no similar library available for it. mreframe
is meant to deal with this issue; however, after some thinking,
I’ve decided to make it a regular JS library instead (since Wisp would interop with it seamlessly anyway).
To minimize dependencies (and thus keep the library lightweight as well, as well as make it easy to use), mreframe
uses
Mithril in place of React; it also has no other runtime dependencies. In current version, it has size
of 10Kb (4Kb gzipped) by itself, and with required Mithril submodules included it merely goes up to 26Kb (9.5Kb gzipped).
The library includes two main modules: reagent
(function components modelling DOM with data literals),
and re-frame
(state/side-effects management). You can decide to only use one of these as they’re mostly
independent of each other (although re-frame
uses reagent
atoms internally to trigger redraws on state updates). It also
includes atom
module for operating state (though you can avoid operating state atoms directly), as well as
util
module for non-mutating data updates (these were implemented internally to avoid external dependencies).
Both reagent
and re-frame
were implemented mostly based on their
reagent.core
and
re-frame.core
APIs respectively, with minor changes to account for the switch
from ClojureScript to JS and from React to Mithril. The most major change would be that since Mithril relies on minimizing
calculations rather than keeping track of dependency changes, state atoms in mreframe
don’t support subscription mechanisms
(they do however register themselves with the current component and its ancestors to enable re-rendering detection);
also, I omitted a few things like global interceptors and post-event callbacks from re-frame
module, and added a couple
helper functions to make it easier to use in JS. And, of course, in cases where switching to camelCase would make an identifier
more convenient to use in JS, I did so.
For further information, I suggest checking out the original (ClojureScript) reagent
and re-frame
libraries documentation. Code examples specific to mreframe
can
be found in the following Examples section, as well as in the API reference.
Usage
Install the NPM package into a project with npm i mreframe
/yarn add mreframe
;
or, import as a script in webpage from a CDN: <script src="https://unpkg.com/mreframe/dist/mreframe.min.js"></script>
.
(If you want routing as well, use this:
<script src="https://unpkg.com/mreframe/dist/mreframe-route.min.js"></script>
.)
Access in code by requiring either main module:
let {reFrame: rf, reagent: r, atom: {deref}, util: {getIn}} = require('mreframe');
or separate submodules:
let rf = require('mreframe/re-frame');
let {getIn} = require('mreframe/util');
In case you’re using nodeps bundle, or if you want to customize the equality function used by mreframe, run _init
first:
rf._init({eq: _.eq});
_init
is exposed by reagent
submodule (affects only the submodule itself), and also by re-frame
and the main module
(affects both re-frame
and reagent
submodules).
mreframe/atom
module implements a data storing mechanism called atoms;
the main operations provided by it are deref(atom)
which returns current atom value, reset(atom, value)
which replaces
the atom value, and swap(atom, f, ...args)
which updates atom value (equivalent to reset(atom, f(deref(atom), ...args))
).
For further information, see API reference below and the following tutorials / live demo pages: Reagent (components), re-frame (events/state management).
Q & A
- Q: It says I shouldn’t mutate the data stored in atoms; how do I update it in that case?
A: Non-mutating updates can be done using functions frommreframe/util
, or a full-scale functional library like Lodash / Ramda (/ Rambda). - Q: How do I inject raw HTML?
A: If you absolutely have to, usem.trust
. Indist/mreframe.min.js
it can be accessed asrequire('mithril/hyperscript').trust()
. - Q: What about routing?
A: Use Mitrhil routing API. Indist/mreframe-route.min.js
it can be accessed asrequire('mithril/route')
. - Q: What if I want to use a custom version Mithril (e.g. full distribution or a different version)?
A: If you’re using JS files from CDN, pickdist/mreframe-nodeps.min.js
instead, and load Mithril as a separate script; then runrf._init
to connect them. - Q: Are there any third-party libraries (components etc.) I can use with this?
A: Yes, pretty much any Mithril library should be compatible. - Q: How stable is this API?
A: The Reagent + re-frame combination has existed since 2015 without much change; as I’m reusing it pretty much directly, there’s no reason to change much for me either (the only breaking changes so far were in v0.1 update, where I properly implemented subscription detection/redraw cutoff). - Q: And how performant is this thing?
A: Mithril boasts high speed in raw rendering;mreframe/reagent
naturally slows it down to an extent (up to several times), but in v0.1 a redraw cutoff was added, which greatly reduces recalculated area in complex pages with large amount of components. (See render performance comparison for Mithril and mreframe – though they’re mostly testing raw render performance) - Q: I have a huge amount of DB events per second in my app, can I disable the deep-equality check in
db
effect handler?
A: Specifyeq
inrf._init
to replace it with eithereqShallow
orindentical
. - Q: I hate commas and languages that aren’t syntactical supersets of JS. Can I still use this somehow?
A: Well if you absolutely must, you can use JSX. (Note that JSX is not exatly a great match for Reagent components.)
Examples
- Reagent form-2 components + Reagent/Mithril interop (scripted in JavaScript) [source]
- Re-frame state/side-effects management with Reagent components (scripted in CoffeeScript) [source]
- Routing using
m.route
(from external Mithril bundle, connected via_init
) (scripted in Wisp) [source] - Routing using
mithril/route
(frommreframe-route.js
) (scripted in JavaScript) [source] - Rendering HTML from Reagent components using
mithril-node-render
(scripted in CoffeeScript) [source] - JSX usage example [source]
API reference
mreframe
exposes following submodules:
util
includes utility functions (which were implemented in mreframe to avoid external dependencies and were exposed so that it can be used without dependencies other than Mithril);atom
defines a simple equivalent for Clojure atoms, used for controlled data updates (as holders for changing data);reagent
defines an alternative, Hiccup-based component interface for Mithril;re-frame
defines a system for managing state/side-effects in a Reagent/Mithril application.
There’s also jsx-runtime
which isn’t included in main module (it implements JSX support).
Each of these can be used separately (require('mreframe/<name>')
), or as part of the main module
(require('mreframe').<name>
; .reFrame
in case of re-frame
module). Note that the nodeps bundle doesn’t load
Mithril libraries by default (so you’ll have to call the _init
function which it also exports).
As most of these functions are based on existing ClojureScript equivalents, I’ll provide links to respective CLJ docs
for anyone interested (although, if you’re familiar with these concepts, you’ll get the idea from the function name
in most cases). A major difference, of course, is that instead of vectors, JS arrays are used, and dictionaries
(plain objects) are used instead of maps; instead of keywords, strings are uses (:foo
→ 'foo'
).
Since Wisp does the same,
using mreframe
with Wisp makes for mostly identical code to that of CLJS
reagent
/re-frame
(at least in regular usecases).
mreframe
module API:
mreframe/re-frame
module API:
- setup:
rf._init
(normally not needed); - events (decision-making logic defined as pure functions):
- registering functions (
rf.regEventDb
,rf.regEventFx
,rf.regEventCtx
), - dispatching functions (
rf.dispatch
,rf.dispatchSync
), - unregistering function for development (
rf.clearEvent
), - helper function
rf.purgeEventQueue
(for cancelling scheduled events);
- registering functions (
- subscriptions (computations for views, with caching):
- registering function (
rf.regSub
), - querying functions (
rf.subscribe
,rf.dsub
), - unregistering function for development (
rf.clearSub
), - cache clearing function for development (
rf.clearSubscriptionCache
);
- registering function (
- effects (implementation of side-effects for use in events):
- registering function (
rf.regFx
), - unregistering function for development (
rf.clearFx
), - helper function
rf.disp
(for dispatchingonSuccess
/onFailure
events), - builtin effects (
db
,fx
,dispatchLater
,dispatch
);
- registering function (
- interceptors (‘wrappers’ that alter event processing when used in event registering function):
- creator function (
rf.toInterceptor
), - predefined interceptors (
rf.unwrap
,rf.trimV
) and generators (rf.path
,rf.enrich
,rf.after
,rf.onChanges
), - helper functions (
rf.getCoeffect
,rf.assocCoeffect
,rf.getEffect
,rf.assocEffect
,rf.enqueue
);
- creator function (
- coeffects (‘external’ input getters for events, used as interceptors):
- registering function (
rf.regCofx
), - interceptor creator function (
rf.injectCofx
), - unregistering function for development (
rf.clearCofx
).
- registering function (
mreframe/reagent
module API:
- setup:
r._init
(normally not needed); - atoms:
r.atom
(triggers redraw on update),r.cursor
(‘wrapper’ atom); - component creation functions:
r.adaptComponent
for using Mithril components,r.createClass
for creating Reagent components with hooks;
- component rendering functions:
r.createElement
for directly invoking Mithril hyperscript,r.asElement
for rendering Hiccup,r.with
for supplying metadata (props) to Reagent components,r.render
for mounting Reagent/Hiccup view on DOM,r.resetCache
for clearing function components cache (for development);
- component helper functions:
r.classNames
for generating/combining CSS classes lists,r.curentComponent
for accessing Mithril component from Reagent views,- component data accessors (
r.children
,r.props
,r.argv
,r.stateAtom
), - Reagent component state reader function (
r.state
and updater functions (r.setState
,r.replaceState
).
mreframe/atom
module API:
- regular atom creator function (
atom
); - atom state reader (
deref
); - atom state updaters (
reset
,resetVals
,swap
,swapVals
,compareAndSet
).
mreframe/util
module API:
- general-use functions (
identity
,eq
,eqShallow
,indentical
,chain
,repr
); - type check functions (
type
,isArray
,isDict
,isFn
); - functions for arrays (
chunks
,flatten
); - functions for dicts (
dict
,entries
,keys
,vals
); - functions manipulating collections (
merge
,assoc
,dissoc
,update
,getIn
,assocIn
,updateIn
); - a simple multimethods implementation
(
multi
).