A reactive version of DataScript’s d/entity
API for
your Reagent components.
This library allows you to use a reactive version of DataScript entities directly in your Reagent components, re-rendering those components only when the exact attributes they depend on are updated.
This is true for simple attributes and reference attributes (i.e. references to other entities), which means that you can essentially 'walk' your DataScript data graph within your components, without triggering unnecessary re-renders.
Sorry about this stream-of-consciousness README, I might tidy it up later if this is at all interesting to people.
(ns austinbirch.reactive-entity.counter-demo
(:require [datascript.core :as d]
[austinbirch.reactive-entity :as re]))
(defonce db-conn
(let [conn (d/create-conn)]
(d/transact! conn [{:db/id 1
:counter 0}])
conn))
(defn inc-counter!
[]
(let [counter (-> (d/entity @db-conn 1)
:counter)]
(d/transact! db-conn [{:db/id 1
:counter (inc counter)}])))
(defn counter-demo
[]
(let [;; make reactive entity
<session (re/entity 1)
;; read `:counter` from reactive entity
counter (:counter <session)]
[:div
[:div
[:span "Counter: " (str counter)]]
[:button {:onClick inc-counter!}
"Add one to counter"]]))
(defn ^:dev/after-load mount-root
[]
(re/clear-cache!)
(reagent.dom/render [#'counter-demo] (.getElementById js/document "app")))
(defn init
[]
(re/init! db-conn)
(mount-root))
Add to your project dependencies:
See the austinbirch.reactive-entity.demo namespace.
Assuming you have yarn/npm and shadow-cljs installed, you should be able to clone this repo and run:
yarn install && shadow-cljs watch demo
You’ll be able to view the demo on http://localhost:8080, watching the DevTools console for log output as components re-render.
I’ll put together a better demo if there’s interest.
This library listens to tx-report
s emitted from your DataScript database, and then only re-calculates data for changed
attributes where we also have a component using that data.
tx-report
, and update any
reactions that are currently being used in components.tx-report
s.There’s hardly any code, so it might actually be easier to just read that.
For me, passing around entities can remove a bit of the “what data does this map have?” feeling I usually get when
having a parent component perform a query (for example, using posh), and then
passing down a plain map through component props. ReactiveEntity
instances have access to whatever you store in your DataScript
database, regardless of which data parent components need/have accessed.
ReactiveEntity
acts like a normal Clojure map, so in theory you should be able to test the components/functions by
passing plain maps instead.
(defn todo-message
[{:keys [<todo]}]
[:div [:span (:todo-list.item/todo <todo)]])
(= (todo-message {:<todo {:todo-list.item/todo "Something"}})
[:div [:span "Something"]])
I’m currently using a prefix when naming ReactiveEntity
instances so that I can see the difference quickly. I’m
using <
for now. It’s important to know the difference between data & Reagent atoms/reactions to avoid falling into
the "Reactive deref not supported in seq" problem that affects
all Reagent atoms/reactions.
(defn ui-todo-lists
[]
(let [<session (re/entity [:session/id "session-id"])
<todo-lists (:session/todo-lists <session)]
;;...
))
A work in progress for now. Not in production yet, but very close to being used in production in a couple of complex single-page applications.
Has some foundational tests for entities using :db/id
, lookup-refs, and reverse-ref lookups. Probably could do with some more tests to check that the reactions are being used where possible.
There’s probably a bunch of low-hanging fruit from a performance perspective; I’ve yet to push it too far. I’m happier about the developer affordance of being able to use DataScript entities directly in Reagent components at the moment.
d/q
and d/pull
APIsRead attributes on entities and only re-render components when attributes they depend on change.
(ns austinbirch.reactive-entity.counter-demo
(:require [datascript.core :as d]
[austinbirch.reactive-entity :as re]))
(defonce db-conn
(let [conn (d/create-conn)]
(d/transact! conn [{:db/id 1
:counter 0}])
conn))
(defn inc-counter!
[]
(let [counter (-> (d/entity @db-conn 1)
:counter)]
(d/transact! db-conn [{:db/id 1
:counter (inc counter)}])))
(defn counter-demo
[]
(let [;; make reactive entity
<session (re/entity 1)
;; read `:counter` from reactive entity
counter (:counter <session)]
[:div
[:div
[:span "Counter: " (str counter)]]
[:button {:onClick inc-counter!}
"Add one to counter"]]))
(defn ^:dev/after-load mount-root
[]
(re/clear-cache!)
(reagent.dom/render [#'counter-demo] (.getElementById js/document "app")))
(defn init
[]
(re/init! db-conn)
(mount-root))
Navigate your graph data (via reference attributes) just as you would with the d/entity API, only re-rendering if there are added/removed references.
(ns austinbirch.reactive-entity.multiple-counters-demo
(:require [datascript.core :as d]
[austinbirch.reactive-entity :as re]))
(defonce db-conn
(let [conn (d/create-conn {:session/counters {:db/valueType :db.type/ref
:db/cardinality :db.cardinality/many}
:counter/id {:db/unique :db.unique/identity}})]
(d/transact! conn [{:db/id 1
:session/counters [{:counter/id 1
:counter/count 0}
{:counter/id 2
:counter/count 10}]}])
conn))
(defn inc-counter!
[counter-id]
(let [counter (-> (d/entity @db-conn [:counter/id counter-id])
:counter/count)]
(d/transact! db-conn [{:counter/id counter-id
:counter/count (inc counter)}])))
(defn counter-view
[{:keys [<counter]}]
(let [id (:counter/id <counter) ;; subscribe to changes for `:counter/count` for this counter only
count (:counter/count <counter)]
[:div
[:div
[:span "Counter ID:" id]]
[:div
[:span "Count: " (str count)]]
[:button {:onClick (partial inc-counter! id)}
"Add one to counter"]]))
(defn multiple-counters-demo
[]
(let [;; read all counters as reactive entities
<counters (re/entities :counter/id)]
[:div
(doall
(map (fn [<counter]
[:div {:key (:counter/id <counter)}
;; pass reactive entity as props
[counter-view {:<counter <counter}]])
<counters))]))
(defn ^:dev/after-load mount-root
[]
(re/clear-cache!)
(reagent.dom/render [#'multiple-counters-demo] (.getElementById js/document "app")))
(defn init
[]
(re/init! db-conn)
(mount-root))
Use reactive entities within your re-frame subscriptions, only re-running the subscriptions if the data you access changes
(ns austinbirch.reactive-entity.re-frame-demo
(:require [datascript.core :as d]
[re-frame.core :as rf]
[austinbirch.reactive-entity :as re]))
(defonce db-conn
(let [conn (d/create-conn {:session/todos {:db/valueType :db.type/ref
:db/cardinality :db.cardinality/many}
:todo/id {:db/unique :db.unique/identity}})]
(d/transact! conn [{:db/id 1
:session/todos [{:todo/id 1
:todo/complete? false
:todo/text "Todo 1"}
{:todo/id 2
:todo/complete? true
:todo/text "Todo 2"}
{:todo/id 3
:todo/complete? false
:todo/text "Todo 3"}]}])
conn))
(defn update-todo-status!
[todo-id complete?]
(d/transact! db-conn [[:db/add [:todo/id todo-id] :todo/complete? complete?]]))
(rf/reg-sub
:todos
(fn []
(re/entities :todo/id)))
(rf/reg-sub
:complete-todos
(fn []
(rf/subscribe [:todos]))
(fn [todos]
(filter (fn [<todo]
(:todo/complete? <todo))
todos)))
(rf/reg-sub
:incomplete-todos
(fn []
(rf/subscribe [:todos]))
(fn [todos]
(filter (fn [<todo]
(not (:todo/complete? <todo)))
todos)))
(defn todo-view
[{:keys [<todo]}]
[:div
[:input {:type "checkbox"
:checked (:todo/complete? <todo)
:onChange (fn []
(update-todo-status! (:todo/id <todo)
(not (:todo/complete? <todo))))}]
[:span (:todo/text <todo)]])
(defn todos-demo
[]
(let [complete-todos @(rf/subscribe [:complete-todos])
incomplete-todos @(rf/subscribe [:incomplete-todos])]
[:div
[:div
[:div [:span "Complete todos"]]
(doall
(map (fn [<todo]
[todo-view {:key (:todo/id <todo)
:<todo <todo}])
complete-todos))]
[:div
[:div [:span "Incomplete todos"]]
(doall
(map (fn [<todo]
[todo-view {:key (:todo/id <todo)
:<todo <todo}])
incomplete-todos))]]))
(defn ^:dev/after-load mount-root
[]
(re/clear-cache!)
(reagent.dom/render [#'todos-demo] (.getElementById js/document "app")))
(defn init
[]
(re/init! db-conn)
(mount-root))
Can you improve this documentation?Edit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close