Skip to content

Instantly share code, notes, and snippets.

@orestis
Created May 6, 2020 09:56
Show Gist options
  • Save orestis/546923dd508d7de3e629fef35ad26066 to your computer and use it in GitHub Desktop.
Save orestis/546923dd508d7de3e629fef35ad26066 to your computer and use it in GitHub Desktop.
(ns hooks-store
(:require ["react" :as react]
["react-dom" :as react-dom]))
(defprotocol IStore
(-trigger-subs [this old-state new-state])
(-get-value [this selector])
(destroy [this])
(subscribe [this selector on-change])
(unsubscribe [this k]))
(deftype Store [backing subs watch-key]
IStore
(destroy [this]
(reset! subs {})
(remove-watch backing watch-key))
(-trigger-subs [this old-state new-state]
(doseq [[selector on-change] (vals @subs)]
(let [oldv (selector old-state)
newv (selector new-state)]
(when-not (= oldv newv)
(on-change newv)))))
(-get-value [this selector]
(selector @backing))
(subscribe [this selector on-change]
(let [k (random-uuid)]
(swap! subs assoc k [selector on-change])
k))
(unsubscribe [this k]
(swap! subs dissoc k)
k))
;; borrowed from hx
(defn useValue
"Caches `x`. When a new `x` is passed in, returns new `x` only if it is
not structurally equal to the previous `x`.
Useful for optimizing `<-effect` et. al. when you have two values that might
be structurally equal by referentially different."
[x]
(let [-x (react/useRef x)]
;; if they are equal, return the prev one to ensure ref equality
(let [x' (if (= x (.-current -x))
(.-current -x)
x)]
;; Set the ref to be the last value that was succesfully used to render
(react/useEffect (fn []
(set! (.-current -x) x)
js/undefined)
#js [x'])
x')))
(defn useStore [store selector]
;; aim here is to subscribe to the store on mount,
;; unsubscribe on unmount, and always return the
;; current value of the store
(let [init-v (-get-value store selector)
[init-v setState] (react/useState init-v)
selector' (useValue selector)]
(react/useEffect
(fn []
(let [k (subscribe store selector setState)]
(fn unsub []
(unsubscribe store k))))
#js [store selector'])
;; return the current value
init-v))
(defn new-store [backing]
(let [watch-key (random-uuid)
store (Store. backing (atom {}) watch-key)
watch-fn (fn [_ _ old-state new-state]
(-trigger-subs store old-state new-state))]
(add-watch backing watch-key watch-fn)
store))
(defonce backing-store (atom {}))
(defonce store (new-store backing-store))
(comment
(reset! backing-store {:counter 0})
(swap! backing-store update :counter2 inc)
(-get-value store :counter)
@backing-store
(def k
(subscribe store identity (fn [v] (println "GOT NEW VALUE: " v))))
(destroy store)
(keyword-identical? :counter :counter)
(start)
)
(defn Counter [props]
(println "Counter props" props)
(react/createElement "div" nil "Counter is: " (.-counter props)))
(defn Button []
(react/createElement "button" #js {:onClick (fn [e]
(swap! backing-store update :counter inc))}
"Increase"))
(defn StatefulCounter []
(let [counter (useStore store :counter)]
(react/createElement "div" nil
(react/createElement Counter #js {:counter counter})
(react/createElement Button))))
;; start is called by init and after code reloading finishes
(defn ^:dev/after-load start []
(js/console.log "start")
(react-dom/render
(react/createElement StatefulCounter nil nil)
(js/document.getElementById "app"))
)
(defn ^:export init []
;; init is called ONCE when the page loads
;; this is called in the index.html and must be exported
;; so it is available even in :advanced release builds
(js/console.log "init")
(start))
;; this is called before any code is reloaded
(defn ^:dev/before-load stop []
(js/console.log "stop"))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment