Skip to content

LeXofLeviafan/mreframe

Repository files navigation

Homepage Unit tests Performance tests

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 from mreframe/util, or a full-scale functional library like Lodash / Ramda (/ Rambda).
  • Q: How do I inject raw HTML?
    A: If you absolutely have to, use m.trust. In dist/mreframe.min.js it can be accessed as require('mithril/hyperscript').trust().
  • Q: What about routing?
    A: Use Mitrhil routing API. In dist/mreframe-route.min.js it can be accessed as require('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, pick dist/mreframe-nodeps.min.js instead, and load Mithril as a separate script; then run rf._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: Specify eq in rf._init to replace it with either eqShallow or indentical.
  • 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

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:

mreframe/reagent module API:

mreframe/atom module API:

mreframe/util module API: