Skip to content

Instantly share code, notes, and snippets.

Last active February 16, 2023 21:16
What would you like to do?
Test use of DataScript for state management of Reagent views.
(ns reagent-test.core
(:require [reagent.core :as reagent :refer [atom]]
[datascript :as d]
[cljs-uuid-utils :as uuid]))
(defn bind
([conn q]
(bind conn q (atom nil)))
([conn q state]
(let [k (uuid/make-random-uuid)]
(reset! state (d/q q @conn))
(d/listen! conn k (fn [tx-report]
(let [novelty (d/q q (:tx-data tx-report))]
(when (not-empty novelty) ;; Only update if query results actually changed
(reset! state (d/q q (:db-after tx-report)))))))
(set! (.-__key state) k)
(defn unbind
[conn state]
(d/unlisten! conn (.-__key state)))
;;; Creates a DataScript "connection" (really an atom with the current DB value)
(def conn (d/create-conn))
;;; Add some data
(d/transact! conn [{:db/id -1 :name "Bob" :age 30}
{:db/id -2 :name "Sally" :age 25}])
;;; Maintain DB history.
(def history (atom []))
(d/listen! conn :history (fn [tx-report] (swap! history conj tx-report)))
(defn undo
(when (not-empty @history)
(let [prev (peek @history)
before (:db-before prev)
after (:db-after prev)
;; Invert transition, adds->retracts, retracts->adds
tx-data (map (fn [{:keys [e a v t added]}] (d/Datom. e a v t (not added))) (:tx-data prev))]
(reset! conn before)
(swap! history pop)
(doseq [[k l] @(:listeners (meta conn))]
(when (not= k :history) ;; Don't notify history of undos
(l (d/TxReport. after before tx-data)))))))
;;; Query to get name and age of peeps in the DB
(def q-peeps '[:find ?n ?a
[?e :name ?n]
[?e :age ?a]])
;; Simple reagent component. Returns a function that performs render
(defn peeps-view
(let [peeps (bind conn q-peeps)
temp (atom {:name "" :age ""})]
(fn []
[:h2 "Peeps!"]
(map (fn [[n a]] [:li [:span (str "Name: " n " Age: " a)]]) @peeps)]
[:span "Name"][:input {:type "text"
:value (:name @temp)
:on-change #(swap! temp assoc-in [:name] (.. % -target -value))}]]
[:span "Age"][:input {:type "text"
:value (:age @temp)
:on-change #(swap! temp assoc-in [:age] (.. % -target -value))}]]
{:onClick (fn []
(d/transact! conn [{:db/id -1 :name (:name @temp) :age (js/parseInt (:age @temp))}])
(reset! temp {:name "" :age ""}))}
"Add Peep"]
[:button {:on-click #(undo) :disabled (= 0 (count @history))} "Undo"]])))
;;; Query to find peeps whose age is less than 18
(def q-young '[:find ?n
[?e :name ?n]
[?e :age ?a]
[(< ?a 18)]])
;;; Uses reagent/create-class to create a React component with lifecyle functions
(defn younguns-view
(let [y (atom nil)]
;; Subscribe to db transactions.
(fn [] (bind conn q-young y))
;; Unsubscribe from db transactions.
:component-will-unmount (fn [] (unbind conn y))
(fn [_]
[:h2 "Young 'uns (under 18)"]
(map (fn [[n]] [:li [:span n]]) @y)]])})))
;;; Some non-DB state
(def state (atom {:show-younguns false}))
;;; Uber component, contains/controls stuff and younguns.
(defn uber
[:div [peeps-view]]
[:div {:style {:margin-top "20px"}}
[:input {:type "checkbox"
:name "younguns"
:onChange #(swap! state assoc-in [:show-younguns] (.. % -target -checked))}
"Show Young'uns"]]
(when (:show-younguns @state)
[:div [younguns-view]])
;;; Initial render
(reagent/render-component [uber] (.-body js/document))
Copy link

rmoehn commented Jun 5, 2015

There is no license statement anywhere. I'd like to use parts of this in my MIT license project. Do I have your permission?

Copy link


Copy link

rmoehn commented Jul 9, 2015

Oh, didn't see this. Thank you!

Copy link

love the bind, very handy :)

Copy link

rmoehn commented Aug 25, 2015

The problem is that it doesn't work in all cases. Example:

(require '[datascript :as d])

(def schema {:attr1 {:db/valueType :db.type/ref}
             :attr2 {:db/cardinality :db.cardinality/many}})

(def query '[:find ?x
             :where [?a :attr1 ?t]
                    [?t :attr2 ?x]])

(def conn (d/create-conn schema))
(d/listen! conn 42 (fn [t]
                      (println "'novelty':    " (d/q query (:tx-data t)))
                      (println "query result: " (d/q query (:db-after t)))))

(def txd (d/transact conn [{:db/id -1 :attr2 "value1"}
                           {:db/id -2 :attr1 -1}]))
;; > 'novelty':     #{[value1]}
;;   query result:  #{[value1]}

(d/transact conn [{:db/id 1 :attr2 "value2"}])
;; > 'novelty':     #{}
;;   query result:  #{[value1] [value2]}

As you can see, the novelty is empty even though the result of the query changed. Running the same query on the :tx-data as on the whole database is not enough.

I just realized this. Am I seeing it wrong? Is there a solution to this?

Copy link

thosmos commented Sep 4, 2015

@rmoehn The reason there's nothing in your 2nd novelty is because you're using the :tx-data map, which only contains the novel :attr2 datom. However, the query is doing a join with :attr1, which was only novel on the first transaction. If you look at the second :tx-data, it looks like: [#datascript/Datom [1 :attr2 "value2" 536870916 true]].

One solution is obviously just to query the DB. Another might be for the binding function to save a list of attributes in the query and then check the :tx-data for those, and if it finds one, run the full query. Another would be for the listen! function to do this based on the query and schema and then add a key like :tx-data-refs with the :attr1's datom. Also see the PR about query analysis which references this: tonsky/datascript#12

Copy link

rmoehn commented Sep 17, 2015

@thos37 Thanks for your reply and sorry for looking at it so late. – There seems to be no notification system for Gists.

I knew why it doesn't work and I'm concerned that the Gist doesn't mention that it's rather limited. I switched to querying the :db-after entry, because it's easiest and because I'd have to verify that the cleverer solutions really do work in all cases.

But thanks for your pointers! I didn't know about that PR.

Copy link

Can we have this in Java please?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment