this is an early draft. Better to now look at: https://gist.github.com/mike-thompson-day8/dad5b66c8cd74082326dad6ce331128d
There are big, composite components and there are small, simple ones.
Reagent components are simple components - often just called widgets. They visually represent a simple value like an integer or a string, or a selection. A library like re-com
provides many Reagent components including dropdowns, and Text Input fields and radio buttons.
re-frame components are larger, composite components. They tend to visually represent an entity (a more complicated thing) rather than a single, simple value, and they typically present to the user as a "widget-complex" (many widgets) with a cohesive purpose. For example, a pivot table would be a larger component. It might supports the drag and drop of fields to "shelves" which configures a table showing data rollups, and totals. And it might include widgets which allow for levels to be expanded and collapsed, and perhaps "reset" and "copy to clipboard" buttons (which might need to be greyed out or not subject to the current configuration of the pivot).
Irrespective of being big or small, components have two responsibilities:
- to render a representation of the thing they are modelling. So, first, the user needs to see the current value and, optionally, there may be some "affordances" also rendered, making it clear to the user, how they could manipulate the value.
- to accept and interpret user actions, like clicks, typing, and dragging, performed on that visual representation and, in response, to either change the widget's appearance and/or communicate to the surrounding app how the user wants to change the value.
To perform its function, a component (big or small) needs to:
- obtain the current value and be informed about any updates to that value over time. With larger components, the sub-components need to source sub-parts of the overall entity value being shown/edited.
- communicate user-initiated changes to the value. When the user interacts with a component, they are trying to change a value, which must be communicated to the surrounding app. With a larger, composite component, the user will interact in many ways, making many kinds of adjustments to different parts of the
entity
.
With Reagent components, like re-com
widgets:
- they obtain the current value as args/props. The parent Reagent View will source this data somehow, and then supply it. The child Reagent component will be oblivious to how the data was obtained.
- they communicate changes in value by invoking "callback functions" supplied as args/props. Again, the Reagent component is oblivious to its surrounding context, and what actions the callback might take. We, as programmers, might know that the callback causes a re-frame event to be
dispatched
, but the Reagent component knows nothing ofre-frame
,dispatch
orapp-db
.
Reagent components might not know about re-frame but, unsurprisingly, re-frame components do.
With re-frame components:
- sub-widgets obtain values via a
subscribe
(and not from props, like Reagent components) - signal via
dispatch
(and not by invoking a callback, like Reagent components)
If there are many instances of a re-frame component, how do they subscribe
to "their" specific value? One instance of the component might represent entity A
and needs to subscribe to data for that entity, and another might represent entity B
. How should this happen?
Answer:
- re-frame components need to know the identity of the entity to which they should
subscribe
. - and it should then supply that
identity
when it usessubscribe
- And then, the subscription handler will need to use this identity to locate the entity
An identity
is something that can be used to differentiate one entity from another within app-db
. Typically, an identity
is a sub-path
within app-db
. An identity
is always a piece of data.
At a minimum, that might be a key
in some map
(within app-db
), like "1278" or :outside
. Or it could be the integer index into some vector (again, within app-db
). Or it could be the fully qualified, multistep path
from the root ofapp-db
right down to some leaf element, like [:active "customers" 187]
. Or anything in between.
In theory, an identity
can be anything that can be mapped to data in app-db
, perhaps even in some multistep process. It is just that, practically speaking, a sub-path
tends to be the most natural kind of identity
.
So, when we create a re-frame component, we supply it with the identity
of an entity.
An identity
is just data and we can supply it as an arg
to the component - and for discussion purposes, let's call that arg id
.
Any call to subscribe
within the component will provide that identity, perhaps like this (subscribe [:customers id :name])
. Notice the use of the id
.
And, also, any dispatch
made within the re-frame component will also supply id
. (dispatch [:doctor id :validate-parking true])
. Again, notice the use of id
in the event.
The query handlers for the subscription, and the event handlers for dispatch
, can be written in terms of that identity
provided, allowing them locate the right data in app-db
.
In this way, generalized re-frame components can be created.
So, all good? Are we done? Not quite yet.
For any sufficiently complex component, passing id
around can be a drag. In a bad case, we might end up with the old "prop drilling" problem in which id
is passed deeply into nested layers of a complicated set of sub components.
In such cases, we could use BranchScope. <--- new re-frame feature
That would allow us to place id
into the "environment" of the entire branch representing the re-frame component.
This process can nest. High-level identities can be combined with next-level sub-identities.
Two issues that I see:
dispatch
from the POV of re-frame. Butdispatch
doesn't mean it was a user action - it can come as a result of e.g. a message over a WebSocket connection. Also,dispatch
doesn't mean data changes. In other words, there are user actions that don't change any data, and there are data changes that aren't a result of any user action.sub-identity
can sometimes be derived from theidentity
(and maybe from the data that it points to), but not always. In the end, it can result in passing multipleid
s of different kinds into the same component, or maybe evenid
with aderive-sub-id
function.Regarding the second point. It's a fuzzy line between "a complex component with many parameters" and "completely different components". I must admit, I don't know exactly where that line is, or even how to start attempting to formalize it. Ideally, such a line should not be defined by re-frame or any other library/framework - it should be a job for the user to decide where they stand (unless there's some perfect choice). I really hope that it's possible to achieve such flexibility, but at the same time I'm pretty pessimistic. Likely, some sacrifices will have to be made.