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.
@p-himik
Many thanks for the feedback. It is very useful to get other eyes on these things.
Notes:
app-db
.Result: I added this new section ...
Many Identities
A larger, more complex component can often need more than just a single
identity
.For example, a component might need the
identity
for a list of "things", which act as alternative values a user could choose (think dropdown), and also need to know the identity of the current choice held elsewhere withinapp-db
. This component needs two identities.In other cases, a
sub-idnetity
(for a sub-component) can sometimes be derived from the primaryidentity
. Other times, the primaryentity
contains within it the identity of the other entities.Ultimately, we leave re-frame and start discussing the pros and cons of the "data model" you have created in
app-db
.