re-frame.alpha¶
Registration¶
reg¶
(reg kind & args)
Registers a handler.
kind: what kind of handler to register. Possible vals:
:sub: runs a subscription query which is a map.:legacy-sub: runs a subscription query which is a vector.:sub-lifecycle: creates dataflow nodes, optionally caching them.
reg :sub
(reg :sub query-id signal-fn computation-fn)
Register a signal-fn and a computation-fn for a given query-id.
computation-fn is a function of [signals query-map].
The two functions provide a "mechanism" for creating a node
in the signal graph. When a node of type query-id is needed,
the two functions can be used to create it.
query-id- typically a namespaced keyword (later used in subscribe).signal-fn- optional. Returns the input signals required by this kind of node.computation-fn- computes the value (output) of the node from the input signals and the query.
Later, during app execution, a call to (sub {:re-frame/q ::greet :name "dave"})
will trigger the need for a new ::greet signal graph node (matching the
query {:re-frame/q ::greet :name "dave"}). Re-frame will look up the
associated signal-fn and computation-fn, combining them to create the node.
Just to be clear: calling reg :sub does not immediately create a node.
It only registers a "mechanism" (the two functions) by which nodes
can be created later, when a node is brought into existence by the
use of sub.
Declaring the computation-fn
The last argument to reg must be the actual computation-fn, but the
full declaration can be done in two ways:
- The standard way:
A function accepting two arguments,
input-valuesandquery:
(reg :sub :query-id
(fn [input-values query]
(:foo input-values)))
- Syntactic sugar:
The keyword
:->, followed by a 1-aritycomputation-function:
(reg :sub :query-id :-> computation-fn)
This sugary variation allows you to pass a function that will expect only one argument,
namely the input-values, and entirely omit the query. A typical computation-function
expects two arguments, which can cause unfortunate results when attempting to use
clojure standard library functions, or other functions, in a functional manner.
For example, a significant number of subscriptions exist only to get a value
from the input-values. As shown below, this subscription will simply retrieve
the value associated with the :foo key in our db:
(reg :sub
:query-id
(fn [db _] (:foo db))) ;; :<---- boilerplate
This is slightly more boilerplate than we might want. Instead, we could use a keyword directly as a function:
(reg :sub :query-id :foo)
However, this could be dangerous. Remember that re-frame passes
two arguments to the computation-fn: input-values and query.
If the keyword :foo is missing from the input-values, our :foo
getter will use its second argument as a default value, returning the
query. That would be nonsense - the query is not the computation.
In other words, the computation should have no default output.
To achieve that, we use the token :->.
(reg :sub :query-id :-> :foo)
This form tells re-frame to pass only one argument to your computation-fn,
the input-values. Thus, your :foo getter will safely return nil
when the key is not found.
Beyond keywords, you can provide any 1-arity function.
For more complicated use cases, partial, comp, and anonymous functions
will work as expected.
Declaring the signal-fn
The argument(s) declared after query-id and before computation-fn
define the input signals part of the "mechanism". They specify what input values
"flow" into the computation-fn (as the 1st argument) when it is called.
So, there are three ways to declare the input signals.
But note, the 2nd way, in which a
signal-fn is explicitly supplied, is the most canonical and
instructive. The other two are really just sugary variations.
First Variation: No input signal function given:
(reg :sub :query-id
computation-fn) ;; has signature: (fn [db query] ... ret-value)
the node's input signal defaults to app-db
and, as a result, the value within app-db (a map) is
given as the 1st argument when a-computation-fn is called.
Second Variation: A signal function is explicitly supplied.
(reg :sub :query-id
signal-fn ;; <-- here
computation-fn)
This is the most canonical and instructive of the three variations.
When a node is created, the signal-fn will be called, returning the input signal(s).
as either a singleton, if there is only one, or a sequence if there are many,
or a map with the signals as the values.
The current values of the returned signals will be supplied as the 1st argument to
the computation-fn when it is called - and subject to what this signal-fn returns,
this value will be either a singleton, sequence or map of them (paralleling
the structure returned by the signal function).
This example signal function returns a 2-vector of input signals.
(fn [query]
[(sub {:re-frame/q :a})
(sub {:re-frame/q :b})])
The associated computation function must be written to expect a 2-vector of values for its first argument:
(fn [[a b] query] ;; 1st argument is a seq of two values
...)
If, on the other hand, the signal function was simpler and returned a singleton, like this:
(fn [query]
(sub {:re-frame/q :a})) ;; <-- returning a singleton
then the associated computation function must be written to expect a single value as the 1st argument:
(fn [a query] ;; 1st argument is a single value
...)
Further Note: variation #1 above, in which an signal-fn was not supplied, like this:
(reg :sub :query-id
computation-fn) ;; has signature: (fn [db query] ... ret-value)
is the equivalent of using this
2nd variation and explicitly supplying a signal-fn which returns app-db:
(reg :sub :query-id
(fn [_ _] re-frame/app-db) ;; <-- explicit signal-fn
a-computation-fn) ;; has signature: (fn [db query-vec] ... ret-value)
Third variation: Syntactic sugar
(reg :sub :a-b-sub
:<- [:a-sub]
:<- [:b-sub]
(fn [[a b] query] ;; 1st argument is a seq of two values
{:a a :b b}))
This 3rd variation is just syntactic sugar for the 2nd. Instead of providing an
signals-fn you provide one or more pairs of :<- and a subscription vector.
If you supply only one pair, a singleton will be supplied to the computation function,
as if you had supplied a signal-fn returning only a single value:
(reg :sub :a-sub
:<- [:a-sub]
(fn [a query] ;; only one pair, so 1st argument is a single value
...))
Syntactic sugar for both the signal-fn and computation-fn can be used together
and the direction of arrows shows the flow of data and functions. The example from
directly above is reproduced here:
(reg :sub :a-b-sub
:<- [:a-sub]
:<- [:b-sub]
:-> (partial zipmap [:a :b]))
For further understanding, read the tutorials, and look at the detailed comments in /examples/todomvc/src/subs.cljs.
reg :legacy-sub
(reg :legacy-sub query-id signal computation-fn)
Register a signal-fn and a computation-fn for a given query-id.
For details, see :sub, above. Compared with :sub, :legacy-sub
supports a vector as the query, rather than a map. For instance:
(sub [::greet {:name "dave"}])
In this case, ::greet is the query-id. Re-frame looks up the associated
signal-fn and computation-fn and computes the output of the node,
creating it if necessary. It passes the entire vector to your computation-fn
as the second argument.
Declaring the computation-fn (:legacy-sub)
Legacy subscriptions have another kind of syntactic sugar,
in addition to those supported by :sub:
The token :=>, followed by a multi-arity computation-function.
(reg :legacy-sub :query-id :=> computation-fn)
A vector query can be broken into two components, [query-id & optional-values].
Some subscriptions require the optional-values for extra work within the subscription.
Canonically, we'd need to destructure these optional-values:
(reg :legacy-sub :query-id
(fn [db [_ foo]] [db foo]))
Again, we are writing boilerplate just to reach our values. Instead, we might prefer to
have direct access through a parameter vector like [input-values optional-values].
That way, we could provide a multi-arity function directly as our computation-fn:
(reg :legacy-sub :query-id :=> vector) ;; Could also be (fn [db foo] [db foo])
Compatibility:
Query-maps are a more recent development for re-frame.
They are implemented in a backwards-compatible way.
That means you can pass either a vector or a map to sub,
regardless of what kind of "mechanism" was registered for
that query-id (whether via :sub or :legacy-sub).
If you call (reg :sub ...), you have registered a computation-fn
and signal-fn which both expect to be passed a query-map.
If you then call (sub [...]), passing a query-vector, re-frame will
transparently adapt your vector, passing an equivalent map to your functions.
Likewise, invoking a :legacy-sub mechanism with a query-map
will cause re-frame to pass along an equivalent vector.
When this conversion is done, re-frame includes the original form
in the metadata (using either :re-frame/query-m or :re-frame/query-v).
When converting from a map to a vector, :re-frame/query-v is used, if present.
Otherwise, the original map is included as the
second item (commonly thought of as the "params" of that query).
For instance, the query:
{:re-frame/q ::items}
converts to:
^{:re-frame/query-m {:re-frame/q ::items}}
[::items {:re-frame/q ::items}]
Likewise, the query:
[::items 1 2 3]
converts to:
{:re-frame/q ::items
:re-frame/lifecycle :safe
:re-frame/query-v [::items 1 2 3]}
Note that since 1 2 3 aren't named, there's no way to represent them
in the map. Instead, they can be destructured from the :re-frame/query-v key.
This :re-frame/query-v is also taken into consideration when converting a
map to a vector. For instance:
{:re-frame/q ::items
:re-frame/lifecycle :reactive
:re-frame/query-v [::items 1 2 3]}
converts to:
^{:re-frame/query-m {:re-frame/q ::items
:re-frame/lifecycle :reactive}}
[::items 1 2 3]
reg :sub-lifecycle
(reg :sub-lifecycle lifecycle-id lifecycle-fn)
A lifecycle-fn controls how dataflow nodes are created, sometimes managing their lifecycle within a cache (i.e. the signal graph).
When sub (or subscribe) is called, re-frame uses information within the query
to look up and call the associated lifecycle-fn. The lifecycle-fn returns the actual dataflow node.
It can do other things along the way - primarily, it can make use of a cache. This cache of nodes
can make a subscription more performant. Instead of creating a node and calculating
its value every time you call sub, re-frame can look up nodes it has already created,
along with their previous calculations.
The lifecycle-id determines which lifecycle-fn a subscription will use.
When (sub query) is called, re-frame always derives the lifecycle-id from the query:
- Map queries can specify this with a
:re-frame/lifecyclekey. - Vector queries can include the
:re-frame/lifecyclekey in the vector's metadata.
When a query does not explicitly declare a lifecycle,
re-frame uses the :safe lifecycle by default.
Writing a lifecycle-fn
Whenever sub is called, re-frame determines the lifecycle-id from the query.
Then, it calls the associated lifecycle-fn, passing it the query.
The task of a lifecycle-fn is return the dataflow node necessary to calculate
the output value for query. Along the way, it can also store the node
within a cache (a.k.a. the signal graph).
The re-frame.query namespace provides some helper functions for this:
cached
Accepts a query.
Returns the matching dataflow node from the signal graph,
or nil if it is not found.
handle
Accepts a query.
Returns a new dataflow node.
cache!
Accepts a query and a dataflow node.
Adds the dataflow node to the signal graph.
The node can be looked up later, using cached.
clear!
Accepts a query.
Removes the associated dataflow node from the signal graph.
lifecycle-fn example
For demonstration, here is a "sliding" lifecycle-fn which caches the last three queries:
(def history (atom []))
(def size 3)
(def overflow? #(> (count @history) size))
(def slide! #(swap! history (comp vec rest)))
(defn sub-sliding [q]
(if-let [cached-node (q/cached q)]
cached-node
(let [new-node (q/handle q)]
(q/cache! q new-node)
(swap! history conj q)
(when (overflow?)
(q/clear! (first @history))
(slide!))
new-node)))
(reg :sub-lifecycle :sliding sub-sliding)
You can use your new lifecycle by declaring :re-frame/lifecycle:
(re-frame.alpha/sub ^{:re-frame/lifecycle :sliding} [:hi 1]})
After subscribing to more than three different queries, the sliding behavior will happen, clearing some of the corresponding dataflow nodes from the cache.
(re-frame.alpha/sub ^{:re-frame/lifecycle :sliding} [:hi 2]})
(re-frame.alpha/sub ^{:re-frame/lifecycle :sliding} [:hi 3]})
(re-frame.alpha/sub ^{:re-frame/lifecycle :sliding} [:hi 4]}) ;; now [:hi 1] is cleared
Note: Lifecycles are an alpha feature. Don't expect re-frame.core to work the same way.
It's totally valid to add metadata to a query, but re-frame.core will ignore it.
For instance, this subscription creates a dataflow node with the :reactive lifecycle,
even though you've "declared" something else:
(re-frame.core/subscribe ^{:re-frame/lifecycle :sliding} [:hi 4]})
On the other hand, re-frame.alpha can make use of nodes created by re-frame.core.
Specifically, the :safe lifecycle also checks for an equivalent query
in the cache of :reactive nodes.
That way, you can subscribe to a query using plain old re-frame.core/subscribe within a view function,
and, elsewhere, subscribe to that same query using re-frame.alpha/sub, without needing to recalculate.
Standard lifecycles
Re-frame provides these lifecycles. The default lifecycle is :safe.
:no-cache Creates a dataflow node and eagerly computes a subscription's value,
without storing the node in the signal graph.
If the subscription depends on other subscriptions, then re-frame will try to dispose them,
effectively making them :no-cache as well (as long as nothing else depends on them).
Thus, input signals are also cleared from memory when they aren't needed.
Note: Technically, if an input subscription has inputs of its own, those won't get cleared from memory. In practice, these deps-of-deps will be layer-2 subscriptions, which should be simple enough to avoid leaking memory anyway. Otherwise, you could write a new lifecycle-fn to recursively cleanup the full dependency tree.
:safe subscribes in a memory-safe way, using the cache when it can,
and eagerly computing its output when it can't.
Re-frame uses :safe by default, whenever you use re-frame.alpha/sub
or re-frame.alpha/subscribe without declaring a lifecycle explicitly.
There is one exception to this memory-safety, which should be negligible (see :no-cache for details).
If your dataflow node is already cached (for instance, by calling sub in the render-fn of a mounted component),
then :safe will use the cached node, skipping the eager computation and cleanup.
Re-frame also checks the cache for an equivalent node with the :reactive lifecycle.
If such a node exists, re-frame uses it directly, without re-calculating or touching the cache.
Consequently: When there are no reagent components mounted which depend on the subscription, then the subscription will recalculate its output each time it's called, even when its inputs are the same.
But, as long as there are reagent components, then caching and deduplication will
work as normal. In most cases, we expect :safe to be a reasonable tradeoff
between performance and memory-safety.
:forever Creates a long-lived subscription.
Re-frame looks up the cached subscription, creating it if necessary.
The subscription is permanently cached, never cleared. Re-frame eagerly
runs the subscription on creation, to create its input signals.
This behavior extends to input signals, making :forever effectively "contagious".
If the subscription depends on other subscriptions,
those subscriptions will be permanently stored in the cache,
despite other mechanisms' attempts to clear them (see :no-cache and :reactive).
This is because reagent's internals prevent disposal of "watched" reactions, and
a :forever subscription "watches" its inputs forever.
:reactive Looks up subscription from a cache, creating it if necessary.
The subscription stays in the cache for as long as one or more reagent
components depend on it.
Given a specific query, re-frame stores a set of back-references to
each reagent component which depends on its dataflow node - in other
words, each component which called (sub query) within its render
function. References are added when components call sub, and
removed when they unmount. When the last component unmounts, and no
components depend on the node, then re-frame clears the node.
:reactive is unsafe when called outside a reactive context (in other
words, not inside a reagent component's render function). Since
there is no component, there is no way to clear the node. In cases
where sub is called many times with many different queries, this
effectively leaks memory. Re-frame prints a warning in this case.
Note: for more details on reactive context, see https://day8.github.io/re-frame/flows-advanced-topics/#reactive-context
Subscription¶
sub¶
(sub q)
(sub id q)
Given a query, returns a Reagent reaction which will, over
time, reactively deliver a stream of values. So, in FRP-ish terms,
it returns a Signal.
To obtain the current value from the Signal, it must be dereferenced:
(let [signal (sub {:re-frame/q ::items})
value (deref signal)] ;; could be written as @signal
...)
which is typically written tersely as simple:
(let [items @(sub {:re-frame/q ::items})]
...)
query is a map containing:
:re-frame/q: Required. Names the query. Typically a namespaced keyword.:re-frame/lifecycle: Optional. See docs forreg :sub-lifecycle.
The entire query is passed to the subscription handler. This means you can use
additional keys to parameterise the query it performs.
Example Usage:
(require '[re-frame :as-alias rf])
(sub {::rf/q ::items
::rf/lifecycle ::rf/reactive
:color "blue"
:size :small})
Note: for any given call to sub there must have been a previous call
toreg, registering the query handler (functions) associated withquery-id`.
Flows
Flows have their own lifecycle, and you don't need to provide an ::rf/lifecycle key.
To subscribe to a flow, simply call:
(sub :flow {:id :your-flow-id})
Legacy support
dyn-v is not supported.
See also: reg
Flows¶
reg-flow¶
(reg-flow flow)
(reg-flow id flow)
Registers a flow.
A full tutorial can be found at https://day8.github.io/re-frame/Flows
Re-frame uses the flow registry to execute a dataflow graph.
On every event, re-frame runs each registered flow.
It resolves the flow's inputs, determines if the flow is live, and if so,
evaluates the output function, putting the result in app-db at the :path.
A flow is a map, specifying one dataflow node. It has keys:
:id - uniquely identifies the node.
- When a
flowis already registered with the same:id, replaces it. - You can provide an
idargument toreg-flow, instead of including:id.
:inputs - a map of keyword->input. An input can be one of two types:
- vector: expresses a path in
app-db. - map: expresses the output of another flow, identified by a
::re-frame.flow.alpha/flow<-key. Call there-frame.alpha/flow<-function to construct this map.
:output - a function of the keyword->resolved-input map returning the output value of the node.
- A resolved vector input is the value in
app-dbat that path. - A resolved
flow<-input is the value inapp-dbat the path of the named flow. - Re-frame topologically sorts the flows, to make sure any input flows always run first.
- Re-frame throws an error at registration time if any flow inputs form a cycle.
:path - specifies the app-db location where the :output value is stored.
:live-inputs - a map of keyword->live-input for the :live? function.
- A
live-inputworks the same way aninput.
:live? - a predicate function of the keyword->resolved-live-input map, returning the current lifecycle state of the node.
:cleanup - a function of app-db and the :path.
- Returns a new
app-db. - Runs the first time
:live?returnsfalse - Runs when the flow is cleared (see
re-frame.alpha/clear-flow).
The only required key is :id. All others have a default value:
:path:idifidis sequential, otherwise[id].:inputs:{}:output:(constantly true):live?:(constantly true):live-inputs:{}:cleanup:re-frame.utils/dissoc-in
clear-flow¶
(clear-flow)
(clear-flow id)
Arguments: [id]
Deregisters a flow, identified by id.
Later, re-frame will update app-db with the flow's :cleanup function.
If clear-flow is invoked by the :clear-flow effect, this cleanup happens in the :after phase of the same event returning :clear-flow.
If you call clear-flow directly, cleanup will happen on the next event.
get-flow¶
(get-flow db id)
Returns the value within db at the :path given by the registered flow
with an :id key equal to id, if it exists. Otherwise, returns nil.
flow<-¶
(flow<- id)
Creates an input from a flow id.
Legacy Compatibility¶
subscribe¶
Equivalent to sub.
Uses the :safe lifecycle by default, compared with re-frame.core, which uses :reactive.
Flows each have their own lifecycle and are not cached using the same mechanism as subscriptions.
Call (subscribe [:flow {:id :your-flow-id}]) to subscribe to a flow.