WARNING: We discuss below the internal architecture of
Web/MX
, likely of interest only to other UI architects.
Web/MX delivers a simple yet powerful developer experience through several unconventional design choices, none unique to Web/MX, but most executed differently in important ways:
transparent, fine-grained reactivity: the underlying Matrix state manager transparently detects property-to-property dependencies. It uses that information to keep state self-consistent when any property changes. By contrast, almost every UI framework has a "bulk" dependency of a so-called view function on any number of subscriptions to more or less granular nodes in an external store, in the Facebook Flux model. Exceptions here are (the original) MobX and SolidJS, both of which handle fine-grained granularity transparently.
the application is the database: state is managed "in place", gathered locally by app components as needed. No defining, updating, or accessing a separate store; and
global reach: the formula for a derived property of a widget can read any property of any other widget. Any event handler can mutate any property.
all reactive all the time: where we want to use a library such as localStorage, or XHR, or a charting library, we do not have to wrap it in Matrix, but doing so will extend the overall reactive win more than commensurately.
HTML and CSS remain as is.
Here is what all that means to the Web/MX developer:
We have described a lot of easy expressiveness, but are capabilities such as "ask anybody anything" or "fire at will" manageable? At scale? Is that not why we need a secondary store, in the Flux pattern? We can explain why this works, but first, here are two live existence proofs you can try now:
Now why it works, in Q&A form:
Q: How does unfettered state dependency work, without a "separate store" as a single source of truth where integrity can be enforced?
A: We still have a DAG. As formulas for specific widget properties run, and as those formulas read other properties, Matrix quietly weaves a one-way DAG in memory, recording dependencies between computed and read properties. We call this in-place state management
.
Q: What about changing state, without having pre-defined transactions to control change?
A: Because a Web/MX app defines a property-to-property DAG, with a full record of specific dependencies, Matrix internals can propagate any change to any affected properties completely, consistently, and non-redendantly. A secondary store transaction is code a developer thinks will preserve consistency. In a sense, MX formulas are natural transactions we need not think about.
A state manager, after a change, must know:
If those questions are not answered well after a state change, we risk:
Let us look at some code that addresses all that.
Follow these steps to clone Web/MX itself and run an example.
In a terminal:
git clone https://github.com/kennytilton/web-mx.git
cd web-mx
clojure -M -m figwheel.main --build intro-clock --repl
In a minute, look for this to appear in your browser at localhost:9500/intro-clock:
Click "Refresh" to see the time. The code, with tutorial comments:
(declare refresh-button)
(defn manual-clock []
(div {:class [:intro :ticktock]}
(h2 "The time is now....")
(div {:class "intro-clock"
:content (cF (if-let [now (mget me :now)] ;; mget, the standard MX getter, can be used from any code,
(-> now .toTimeString ;; but transparently establishes a dependency, or "subscribes",
(str/split " ") first) ;; if called within a formula.
"__:__:__"))}
{:name :the-clock
:now (cI nil)}) ;; cI for "cell Input"; procedural code can write to these
(refresh-button)))
(defn refresh-button []
(button
{:class :pushbutton
:onclick #(let [me (evt-md %)
; evt-md ^^ derives the MX model from the event;
; Next, we search the family up from me (fmu) to find
; the model named :the-clock...
clock (fmu :the-clock me)]
; ...and reset its property :now, transparently triggering
; full propagation across the DAG:
(mset! clock :now (js/Date.)))}
"Refresh"))
(exu/main #(md/make ::intro
:mx-dom (manual-clock)))
Let us pause to highlight specifically where each unconventional choice manifests itself in concrete code:
now
state, which others can read or mutate reactively;content
consumes the clock now
property, and the button handler alters the same property now
;fmu
or other navigation utilities, widgets have unfettered access to application state; andSo far, so simple. Will it stay that way as we elaborate the app? We continue.
Our clock is accurate, but requires manual intervention to see the latest time. Not fun. Let's have it run by itself.
Exercise #1:
In the function manual-clock
, add this line after the line :name :the-clock
:
:ticker (cF (js/setInterval #(mset! me :now (js/Date.)) 1000))
Save and the clock should run by itself, driven by async mutation of the now
property.
That is great, but now let us allow the user to control things.
Exercise #2
Examine the source of the running-clock
function to see how the crucial TICKING?
property is used to give the user control. For your convenience:
(defn start-stop-button []
(button
{:class :pushbutton
:onclick #(mswap! (fmu :the-clock (evt-md %)) :TICKING? not)}
(if (mget (fmu :the-clock me) :TICKING?)
"Stop" "Start")))
(defn running-clock []
(div {:class [:intro :ticktock]}
(h2 "The time is now....")
(div {:class "intro-clock"
:style (cF (str "color:"
(if (mget me :TICKING?) "cyan" "red")))
:content (cF (if-let [now (mget me :now)]
(-> now .toTimeString (str/split " ") first)
"__:__:__"))}
{:name :the-clock
:now (cI nil)
:ticking? (cI false)
:ticker (cF+ [:watch (fn [prop-name me new-value prior-value cell]
(when (integer? prior-value)
(js/clearInterval prior-value)))]
(when (mget me :TICKING?)
(js/setInterval #(mset! me :now (js/Date.)) 1000)))})
(start-stop-button)))
Things to note:
:watch
function on the ticker
property. Watch
functions fire when a property changes; here we just scavenge intervals;color
property of the clock style;me
and navigates the DAG from there to mutate state as needed.That is how we build applications with Web/MX
:
In the next exercise we will build a modestly richer stopwatch app, and see how well those ingredients hold up.
Can you improve this documentation?Edit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close