Lightweight syntax for interacting with the re-frame
db.
- No boilerplate: eliminate tedious, repetitive code.
- Fewer invented names: many operations are described by the structure of your data + functions that operate on it.
- A functional interface: interact with the re-frame-db by calling functions (named events are also available).
(db/get :a)
(db/get-in [:a :b])
These functions are backed by subscriptions, which update reactively in a Reagent component or reaction.
To maintain performance as your db grows, get-in
uses an intermediate subscription for each segment of the path. This can dramatically reduce the number of comparisons performed each time the db changes.
(db/assoc! :a 1)
(db/assoc-in! [:b :c] 1)
(db/update! :counter inc)
(db/update-in! [:counters :a] inc)
re-frame-simple
functions delegate to a standard set of re-frame events based on core Clojure functions:
[:db/get <key> <?not-found>]
[:db/get-in <path> <?not-found>]
[:db/update <key> <update-function> & <args>]
[:db/update-in <path> <update-function> & <args>]
[:db/assoc <key> <value>]
[:db/assoc-in <path> <value>]
[:db/swap <update-function> & <args>]
[:db/identity]
re-frame-simple
is a light syntax on top of re-frame
which introduces fewer words and concepts and feels more like ordinary Clojure. Using paths into data and plain functions as 'identifiers' for transactions means that whether your system remains legible depends on the structure of your data and the functions you define to manipulate it, rather than names you invent to represent events and queries.
re-frame-simple
is a library, not a fork. It can be happily used in conjunction with existing re-frame
code.
Add the dependency in your deps.edn
or project.clj
(instructions)
Require the namespace:
(ns my-app.core
(:require [re-frame-simple.core :as db]))
Note we've aliased re-frame-simple.core
as db
.
Read from the db using get
, get-in
and identity
functions:
;; value of the entire db
(db/get :a) => (get @app-db :a)
;; swap! the entire db
(db/get-in [:a :b]) => (get-in @app-db [:a :b])
Well, that was simple. What's so special?
Behind the scenes, get
and get-in
map to re-frame subscriptions, so when you use these functions to read data, the component you're in will automatically update when that data changes.
Write using assoc!
, update!
, assoc-in!
, update-in!
:
(db/assoc! :a 1)
(db/assoc-in! [:a :b] 1)
(db/update! :a inc)
(db/update-in! [:a :b] inc)
These functions map to re-frame event handlers which mutate the current state of the world (the db). They end in !
as a reminder that you're mutating the world.
Also available are some operations for the whole db (rather than at a particular key or path). We'll use these less often, as it's easier to understand, inspect, and debug your app when operations are scoped to specific paths.
(db/identity) => @app-db
(db/swap! merge {:a 1})
Here is a counter widget which uses get-in
and update-in
to read and write a counter, given an id.
(defn counter
"Render an interactive counter for `id`"
[id]
;; NOTICE: `db/update-in!` to write
[:div {:on-click #(db/update-in! [::counters id] inc)}
(str "Counter " id ": ")
;; NOTICE: `db/get-in` to read
(db/get-in [::counters id])])
- We put counters in a namespaced path in the db (
::counters
). This means I can search across my app for the namespaced::counters
keyword, and find every instance where a counter is read or mutated. This makes up for some of the explicitness that is lost by moving away from typical, named re-frame events. - We didn't have to "register" anything to get a simple example like this to work. There is no inventing of names for transactions as simple as incrementing an integer at a path.
Using these tools, legibility of the reactivity system is built in to the design of your data structure, instead of added via explicitly named events/actions. (but we can still add those, when desired.)
defupdate
associates a keyword with an update function. This can be dispatched like any other re-frame handler.
(db/defupdate :initialize [db start-val]
(assoc db ::counters start-val))
Use with re-frame.core/dispatch
(it's copied into the db namespace):
(db/dispatch [:initialize {"A" 0
"B" 1
"C" 2}])
Use defquery
to create named queries that read data using db/get
and db/get-in
:
(db/defquery counter-list
"Return the list of counters in the db, by id."
[]
(-> (db/get ::counters)
(keys)))
The function returns a plain value, but uses a reactive subscription behind the scenes to trigger reactivity, so a component that uses the query will update when its data changes.
Usage:
(defn root-view
"Render the page"
[]
[:div
"Click to count!"
;; NOTICE: using our query
(doall (for [id (counter-list)]
^{:key id} [counter id]))])