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,
requireit 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.jsit can be accessed asrequire('mithril/hyperscript').trust(). - Q: What about routing?
A: Use Mitrhil routing API. Indist/mreframe-route.min.jsit 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.jsinstead, and load Mithril as a separate script; then runrf._initto 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/reagentnaturally 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
dbeffect handler?
A: Specifyeqinrf._initto replace it with eithereqShalloworindentical. - 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.) - Q: I want to use features of
mreframein multiple independent parts of my webpage; can I implement them as separate “apps”?
A: You can produce an isolated copy ofmreframe/re-frameby invokinginNamespaceon it. (The mainmreframemodule also exposes a function by the same name; it has similar effect, but produces a “copy” of the main module.)
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:
utilincludes 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);atomdefines a simple equivalent for Clojure atoms, used for controlled data updates (as holders for changing data);reagentdefines an alternative, Hiccup-based component interface for Mithril;re-framedefines 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).
Additionally, invoking inNamespace(<name>) produces a copy of the module with isolated re-frame (within the same page).
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:
- setup:
_init(normally not needed); - submodules:
reFrame,reagent,atom,util. - namespacing:
rf.inNamespace(for implementing isolated widgets);
mreframe/re-frame module API:
- setup:
rf._init(normally not needed); - namespacing:
rf.inNamespace(for implementing isolated widgets); - 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/onFailureevents), - 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.adaptComponentfor using Mithril components,r.createClassfor creating Reagent components with hooks;
- component rendering functions:
r.createElementfor directly invoking Mithril hyperscript,r.asElementfor rendering Hiccup,r.withfor supplying metadata (props) to Reagent components,r.renderfor mounting Reagent/Hiccup view on DOM,r.resetCachefor clearing function components cache (for development);
- component helper functions:
r.classNamesfor generating/combining CSS classes lists,r.curentComponentfor accessing Mithril component from Reagent views,- component data accessors (
r.children,r.props,r.argv,r.stateAtom), - Reagent component state reader function (
r.stateand 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).