There are two kinds of Components:
Roughly speaking, Reagent Components
handle the simple stuff and re-frame Components
look after the larger and more complex work.
Components have two responsibilities, and two needs.
The responsibilities:
rendering:
communicate user intent: if the value is editted, communicate the user's intent to the surrounding app.
In order to fulfil these two responsibilities, Components have two requirements:
the value
(input)user intent
(output)One requirement is I
and the other is O
so, not surprisingly, we
describe them as the Component's I/O requirements.
The simplest Components are Widgets which represent a single value like an integer, string or selection.
They are easily created from base HTML elements, like <input>
or <select>
,
or there are libraries like re-com
which has dropdowns, Text Input fields
and radio buttons.
You can create these Components using only Reagent (no re-frame) and,
for that reason they are called Reagent Components
. Here's an example:
(defn simple-text-input
[value callback-fn]
[:input
{:type "text"
:value value ;; initial value
:on-change #(callback-fn (-> % .-target .-value))}]) ;; callback with value
You'll notice that the I/O requirements of this Component, are satisfied by the two arguments:
Because both needs are satisfied via arguments, this Component is quite reusable. It works for any string value.
What defines a re-frame component
is that it uses dispatch
and subscribe
to satisfy its I/O requirements:
subscribe
is used to obtain valuesdispatch
is used to communicate events carrying user intentre-frame Components tend to be larger. They often represent an entire entity (not just a single, simple value) and they will probably involve a "complex of widgets" with a cohesive purpose.
Here's an example:
(defn customer-names
[id] ;; customer id
(let [customer @(subscribe [:customer id])] ;; obtain the value
[:div "Name:"
[simple-text-input
(:first-name customer) ;; obtain first-name from the entity
#(dispatch [:customer-change id :first-name %])] ;; first name changed
;; last name
[simple-text-input
(:last-name customer) ;; obtain last-name from the entity
#(dispatch [:customer-change id :last-name %])]])) ;; last name changed
Notes:
re-frame Component
because it uses subscribe
and dispatch
for I/Osimple-text-input
component we created aboveIf an app displays multiple instances of a re-frame Component
, how do these
instances subscribe
to their specific value?
One instance might represent entity A
and will need to subscribe to data for that entity, and another instance might represent entity B
,
meaning its subscription will be different.
How should they each obtain the value for their entity?
Answer:
identity
within the query vector given to subscribe
identity
to obtain the entity's valueidentity
(and the event handler will know how to use it)An identity
is something that can be used to differentiate one entity from
another, within app-db
. In a different technology stack, it might be
called "a pointer" or "a reference" or "a foreign key".
Within re-frame, an identity
could be:
key
like "1278" or :warnings
(for a map within app-db
)app-db
)path
from the root of app-db
right down to some leaf element, like [:lavish :cloth 187]
sub-path
of app-db
Ultimately, all these example identities
are sub-paths within app-db
.
An identity
is always a piece of data. If it is a vector, it is likely a path or subpath.
If it is a simple value, it is probably the key of a map or the index of a vector.
When we create an instance of a re-frame Component, we supply it with the identity
of an entity,
via an argument which, for discussion purposes, we'll call id
.
The customer-names
Component above takes an id
argument. The query vector it provides
to subscribe
includes this id
, so too does the event given to dispatch
.
As a result, this Component is reusable - our application can have many instances of it,
and each can represent a different customer - just supply the customer id
.
Here's how we could use it multiple times on the one page to show many customers:
(defn customer-list
[]
[:div
(for [id @(subscribe [:all-customer-ids])]
^{:key id} [customer-names id])])
Some Components need more than one identity
.
For example, a Component might need:
identity
for the list of alternative "things" a user can choose (think items in a dropdown)identity
for the current choice (value) held elsewhere within app-db
This Component will need two args/props for these two identities
.
Imagine a Component (parent) which has a sub-component (child).
The parent might need to provide its child with a sub-identity
derived/computed from the id
supplied to the parent. Perhaps the sub-identity
is built by conj
-ing a further
value onto the original id
. There are many possibilities.
Or, in another situation, an id
provided to a component might reference an entity which
"contains", within its value, the identity
of a further entity - a reference to a reference.
So, the Component might have to subscribe to the primary entity and then, in a second step,
subscribe to the derived entity.
If we take these ideas far enough, we leave behind discussions about re-frame and start, instead, to
discuss the pros and cons of the "data model" you have created in app-db
.
Have you noticed the need for close coordination between a re-frame Component and the subscriptions and dispatches which service it?
A re-frame Component doesn't stand by itself - it isn't actually the unit of reuse.
The unit of reuse is:
I noted at the beginning that a Component had two I/O
needs. So the unit of reuse is the re-frame Component
plus the mechanism for servicing those needs. That's what should be packaged up and put in your library.
Once you internalise that there are three parts to a reusable Component, you might realise that there is another level of abstraction possible.
Up until now, I've said that a re-frame Component is defined by its use of subscribe
and dispatch
. But, maybe it doesn't have to be.
Here is a rewrite of that earlier Component:
(defn customer-names
[id get-customer-fn cust_change-fn]
(let [customer (get-customer-fn id)] ;; obtain the value
[:div "Name:"
;; first name
[simple-text-input
(:first-name customer) ;; obtain first-name from the entity
#(cust_change-fn id :first-name %)] ;; first name has changed
;; last name
[simple-text-input
(:last-name customer) ;; obtain last-name from the entity
#(cust_change-fn id :last-name %)]])) ;; last name has changed
Notes:
dispatch
or subscribe
anymoreLet's rewrite the customer-list
in terms of this new component:
(defn customer-list
[]
(let [get-customer (fn [id] @(subscribe [:customer id]))
put-customer (fn [id field val] (dispatch [:cust-change id field val]))])
[:div
(for [id @(subscribe [:all-customer-ids])]
^{:key id} [customer-names id get-customer put-customer])])
Notes:
I/O functions
which wrap the subscribes and dispatchesDoes this approach mean the customer-names
Component is now more reusable? Yes it does.
The exact subscription query vector to use is now no longer embedded in
the Component itself. The surrounding application supplies that. The Component
has become even more independent of its context. It is even more reusable and flexible.
Obviously there's always a cost to abstraction. You'll have to crunch the cost benefit analysis for your situation.
BTW, in a more complicated case, you can imagine a Component being provided
with more than just a couple of I/O
functions. Instead, it could be supplied
with a map
which nominates many, many I/O
functions which provide to it
the necessary "access" it requires.
Can you improve this documentation? These fine people already did:
Brandon Ringe & Isaac JohnstonEdit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close