One of the first things I look for in a reactive abstraction is how it keeps fine-grained reactivity deps in the face of composition and decomposition of data. How would you say MX is doing in that aspect?
Funny you should ask. I am just starting to appreciate the value of property-level reactivity. "how it keeps fine-grained reactivity deps in the face of composition and decomposition of data" Not sure what you mean by "composition and decomposition of data". Is this about some individual property having a dependency that reaches down into a huge wodge of data? eg, maybe my entire Amazon purchase history is reflected (somehow) in the color of an icon?
It means how easy can I compose and decompose reactive data without reactivity issues. For instance in React if I have a state:
const person = {name: "...", id: "..."}
const bankAccounts = [{ownerId: "...", accountNo: "...", balance: "..."},...]
If I now pass person and bankAccounts to UserDetails component and Report component:
const UserDetails = ({person}) => {
return (<>
...
<input ... value={person.name}/>
</>)
}
const Report = ({person, bankAccounts}) => {
const report = bankAccounts.map((account) => {
return {
...account,
personId: person.id
}
})
return <SomeTable data={details}>
}
Now every time name is changed by UserDetails , person is updated which means report is updated (even though name does not concern it) and then Report rerenders.
Now this is a very simple example and of course I could break person to name and id states and solve the issue, but the core issue here is that when working with data we usually use a lot of this data manipulation: composition to maps, decomposition to props, mapping, filtering, reducing, etc... and reactive data is still data and expose to all of this data transformation and reactive abstractions not always handle that (I'm not even sure if all of it is handleable 🙂Is that the full example? Looks truncated. What is the reactivity issue?
I think I follow, though my React is rusty. I know it looks for changes in props, and rerenders if it sees my props change, but then we need a setState (or hook equivalent) to prompt React to check all the view functions. So the premise here is that we had a name change and a setState? Perhaps the TodoMVC example addresses this: https://github.com/kennytilton/web-mx/blob/6ad9b836b38a4d51897d0bff8cc5b1e9fb42ff93/src/tiltontec/example/todomvc/todo.cljs#L36 tl;dr: if we want a data object to be MX-reactive, we have to "lift" it into the Matrix, by converting it to a model with cells. A new to-do gets immediately converted to an MX to-do:
(defn make-todo
"Make a matrix incarnation of a todo on initial entry"
[islots]
(apply md/make (flatten (into [] (merge
{:type ::todo
:id (str TODO_LS_PREFIX (uuidv4))
:created (util/now)
;; now wrap mutable slots as Cells...
:title (cI (:title islots))
:completed (cI nil)
:due-by (cI (+ (now) (* 4 24 60 60 1000)))
:deleted (cI nil)})))))
Now it is a property-reactive object. The to-do itself is added to the app to-do list.
:onkeypress #(when (= (.-key %) "Enter")
(let [raw (form/getValue (.-target %))
title (str/trim raw)]
(when-not (str/blank? title)
(mswap! (mget @matrix :todos) :items-raw conj ;; <=============
(make-todo {:title title})))))
The :items-raw list litself is reactive, so anyone concerned with the changing population gets notified.
So the answer to your question may be: if we want MX-style reactivity, we "wrap" or "lift" non-reactive things into the MX with more or less glue code to make them work reactively; this then gives us point dependency, so things do not refresh unnecessarily.
The bottom line is that reactive is a big win, so we go out of our way to make non-reactive things reactive. The good news is that, once we have written the glue code, it is done. The bad news is that Web/MX itself is about 400 lines -- not bad for detailed DOM manipualtion, I guess, but not free.
Another thing, perhaps more responsive: the full TodoMVC example does all sorts of filtering of the raw todo list, pulling them into different subsets:
(md/make ::todo-list
:items-raw (cFn (load-all))
:items (cF (doall (remove td-deleted (mget me :items-raw))))
:items-completed (cF (doall (filter td-completed (mget me :items))))
:items-active (cF (doall (remove td-completed (mget me :items))))
:empty? (cF (empty? (mget me :items))))
...but each element of each subset is still a reactive to-do, with object identity, so any cell (reactive property) that reads the name property of a changed Person will get refreshed.
Does that help?Well, actually I already scanned MX for that property (I was serious when I said that's the first thing I check 😉). Since MX includes an explicit reactive lift or wrap, the question goes: how good is the support for lifting (in the Haskell manner) of data transformations over lifted reactive data? The reason I focus on this aspect is the ergonomics of the abstraction: is there a way for me not to know whether a data is reactive or not? Transforming it as it was normal data, then passing it to the dom (or some other sink) and having the reactive cake too?
Starting to follow, I think.
tl;dr: MX reactivity is conscious, deliberate reactivity.
"is there a way for me not to know whether a data is reactive or not?"
If only practically, no. To read the name of a reactive person, I must code (mget person :name) , not (:name person). MX models are suitably provisioned maps in atoms. I could do (:name @person), but if it is a computed property I am not guaranteed an up-to-date result, and I bypass the transparent subscription. And if I did an update-in or sth, it would work but again not propagate, or be caught by MX internals if I mutate a computed cell, or a fixed cell such as, for existing to-dos, the time it was created. (When existing to-dos get lifted they get slightly different lifting.)
Now if I load a kazillion to-dos from storage, they will start as plain CLJ maps. I would process them as plain data, but when I find the one(s) I want the user to edit, I would lift them into MX "models" to make editing and re-persisting easy. But I would not start with that, because lifting is expensive and would prolly offer nothing to the bulk transformations.
The to-do app, with volume not a concern, starts up by loading all todos from localStorage and lifting them immediately into MX models, but then we see handy accessors like td-completed being used, which themselves use mget.
Deep Thoughts: Any MX formula can navigate the entire app and read any property. Very powerful, but I have been anticipating pushback over the unfettered access. No longer. It just occurred to me: any app using a Flux-like separate store, including re-frame with its app-db, lets view functions subscribe to any subscription they like. Same unfettered access. Same story on the "write" side: Flux app code anywhere is free to dispatch transactions against the separate store at will. Now I do not feel so bad . It does work well in practice. So a new insight: the only difference between Flux reactivity and Web/mx reactivity is that w/mx reactivity extends out to the very leaves of an app, such as the color property of the style attribute of a widget. By contrast, Flux reactivity gets only as far as the view function, which ends up with the task of dispersing subscription state across the entire component. And this applies to mutation as well: we just mutate individual properties with Matrix, where a separate store has predefined transactions of varying complexity. 🤔