An extensible state management system for Clojure/ClojureScript inspired by the Redux library for Javascript.
Include in your project.clj's dependencies':
[com.ben-allred/collaj "0.6.1"]Creates a new store from a reducer function. create store takes an optional initial-state and zero or more
ehancers. If initial-state is not supplied, your reducer function must supply initial state via a zero argument arity.
(ns my-namespace.core
(:require [com.ben-allred.collaj.core :as collaj]))
(defn my-reducer [state [type data]]
(case type
:add-to-list (conj state data)
state))
(def store (collaj/create-store my-other-reducer []))
(defn my-other-reducer
([] 0) ;; initial state
([state action]
(case (first action)
:inc (inc state)
:dec (dec state)
state)))
(def other-store (collaj/create-store my-reducer))The resulting store has two functions: :get-state which returns the current value of the store and :dispatch which
causes your reducer to produce a new state based on the current state and the value dispatched. The value dispatched must
be a sequential clojure data structure (vector list or seq) and the first element in the dispatched value must be a
keyword. The keyword represents the dispatched value "type".
(ns my-namespace.core
(:require [com.ben-allred.collaj.core :as collaj]))
(defn my-reducer [state [type]]
(case type
:inc (inc state)
:dec (dec state)
state))
(let [{:keys [dispatch get-state]} (collaj/create-store my-reducer 0)]
(println (get-state))
(dispatch [:inc])
(println (get-state))
(dispatch [:inc])
(dispatch [:inc])
(dispatch [:inc])
(println (get-state))
(dispatch [:dec])
(println (get-state)))
;; 0
;; 1
;; 4
;; 3
;; => nilThe Reducing function passed to create-store can be composed using functions in the com.ben-allred.collaj.reducers
namespace.
The combine function takes a map of keys to reducers and returns a new reducer whose state is generated and
updated by calling the supplied reducers and collected into a map with the same keys as the map passed in.
(ns my-namespace.core
(:require [com.ben-allred.collaj.core :as collaj]
[com.ben-allred.collaj.reducers :as collaj.red]))
(defn counter
([] 0)
([state _] (inc state)))
(defn list-of-things
([] [])
([state [type data]]
(case type
:add-to-list (conj state data)
state)))
(def my-reducer (collaj.red/combine {:counter counter :list-of-things list-of-things}))
(let [{:keys [dispatch get-state]} (collaj/create-store my-reducer)]
(println (get-state))
(dispatch [:anything])
(println (get-state))
(dispatch [:add-to-list :some-value])
(dispatch [:add-to-list :some-other-value])
(println (get-state)))
;; {:counter 0 :list-of-things []}
;; {:counter 1 :list-of-things []}
;; {:counter 3 :list-of-things [:some-value :some-other-value]}
;; => nilThe assoc function takes an initial reducer (that presumably always returns a map) and assoc's keys and values
by calling each reducer with its isolated part of the state and the action. Any part of the state not assoc'ed with
a specific reducer gets passed to the initial reducer.
(ns my-namespace.core
(:require [com.ben-allred.collaj.core :as collaj]
[com.ben-allred.collaj.reducers :as collaj.red]))
(defn person
([] {})
([state [type person]]
(case type
:set-person person
state)))
(defn friends
([] #{})
([state [type friend]]
(case type
:add-friend (conj state friend)
:add-frienemy (conj state friend)
state)))
(defn enemies
([] #{})
([state [type enemy]]
(case type
:add-enemy (conj state enemy)
:add-frienemy (conj state enemy)
state)))
(def my-reducer (collaj.red/assoc person :friends friends :enemies enemies))
(let [{:keys [dispatch get-state]} (collaj/create-store my-reducer)]
(println (get-state))
(dispatch [:set-person {:name "Jan"}])
(println (get-state))
(dispatch [:add-friend "Bill"])
(dispatch [:add-enemy "Jax"])
(println (get-state))
(dispatch [:set-person {:name "Simon" :favorite-colors #{:yellow :teal} :friends :i-get-assoc'ed-over}])
(println (get-state))
(dispatch [:add-friend "Susan"])
(dispatch [:add-frienemy "Harper"])
(println (get-state)))
;; {:friends #{} :enemies #{}}
;; {:name "Jan" :friends #{} :enemies #{}}
;; {:name "Jan" :friends #{"Bill"} :enemies #{"Jax"}}
;; {:name "Simon" :favorite-colors #{:yellow :teal} :friends #{"Bill"} :enemies #{"Jax"}}
;; {:name "Simon" :favorite-colors #{:yellow :teal} :friends #{"Susan" "Bill" "Harper"} :enemies #{"Jax" "Harper"}}
;; => nilThe assoc-in function takes an initial reducer (that presumably always returns a map) and assoc's in key paths and
values by calling each reducer with its isolated part of the state and the action. Any part of the state not assoc'ed
in with a specific reducer gets passed to the initial reducer.
(ns my-namespace.core
(:require [com.ben-allred.collaj.core :as collaj]
[com.ben-allred.collaj.reducers :as collaj.red]))
(defn person
([] {})
([state [type person]]
(case type
:set-person person
state)))
(defn friends
([] #{})
([state [type friend]]
(case type
:add-friend (conj state friend)
:add-frienemy (conj state friend)
state)))
(defn enemies
([] #{})
([state [type enemy]]
(case type
:add-enemy (conj state enemy)
:add-frienemy (conj state enemy)
state)))
(def my-reducer (collaj.red/assoc-in person [:best :friends] friends [:worst :enemies] enemies))
(let [{:keys [dispatch get-state]} (collaj/create-store my-reducer)]
(println (get-state))
(dispatch [:set-person {:name "Jan"}])
(println (get-state))
(dispatch [:add-friend "Bill"])
(dispatch [:add-enemy "Jax"])
(println (get-state))
(dispatch [:set-person {:name "Simon" :favorite-colors #{:yellow :teal} :best {:friends :i-get-assoc'ed-over}}])
(println (get-state))
(dispatch [:add-friend "Susan"])
(dispatch [:add-frienemy "Harper"])
(println (get-state)))
;; {:worst {:enemies #{}} :best {:friends #{}}}
;; {:name "Jan" :worst {:enemies #{}} :best {:friends #{}}}
;; {:name "Jan" :worst {:enemies #{"Jax"}} :best {:friends #{"Bill"}}}
;; {:name "Simon" :favorite-colors #{:yellow :teal} :best {:friends #{"Bill"}} :worst {:enemies #{"Jax"}}}
;; {:name "Simon" :favorite-colors #{:yellow :teal} :best {:friends #{"Susan" "Bill" "Harper"}} :worst {:enemies #{"Jax" "Harper"}}}
;; => nilThe map-of function takes a key-fn and a reducer and builds a map of non-nil results of calling (key-fn action) by
passing their state into the supplied reducer.
(ns my-namespace.core
(:require [com.ben-allred.collaj.core :as collaj]
[com.ben-allred.collaj.reducers :as collaj.red]))
(defn reducer
([] 0)
([state [type]]
(case type
:add (+ state value)
:subtract (- state value)
state)))
(let [{:keys [get-state dispatch]} (collaj/create-store (collaj.red/map-of (comp :key second) reducer))]
(println (get-state))
(dispatch [:add {:key :number-1}])
(dispatch [:subtract {:key :number-2}])
(dispatch [:add {:key :number-2}])
(dispatch [:another-type {:key :number-1}])
(dispatch [:subtract {}])
(println (get-state)))
;; nil
;; {:number-1 1 :number-2 0}
;; => nilThe comp function composes reducers into a single reducer producing the new state by threading it through
every reducer from right to left. Only the first reducer is used to produce initial state.
(ns my-namespace.core
(:require [com.ben-allred.collaj.core :as collaj]
[com.ben-allred.collaj.reducers :as collaj.red]))
(defn reducer-1
([] #{})
([state [type value]]
(case type
:remove (disj state value)
state))
(defn reducer-2
([] ::never-gonna-see-me)
([state [type value]]
(case type
:add (conj state value)
state))
(let [{:keys [get-state dispatch]} (collaj/create-store (collaj.red/comp reducer-1 reducer-2))]
(println (get-state))
(dispatch [:add :some-value])
(dispatch [:add :some-other-value])
(dispatch [:remove :some-value])
(println (get-state)))
;; #{}
;; #{:some-other-value}
;; => nilEnhancers exist in the com.ben-allred.collaj.enhancers namespace. These can be mixed and matched when creating your store by
passing any of them as additional arguments to create-store. The resulting enhancements happen in the order they are
passed to create-store. Typically, you'll need enhancers that alter the data being dispatched to be first in the list
(such as with-fn-dispatch and with-keyword-dispatch). You will likely want with-log-middleware last - or at least
after with-fn-dispatch - because you may want to see the actions that are actually hitting your reducer and probably
don't need to see functions stringified in your logs. Keep this in mind when using these (or other) store enhancers.
The with-subscribers enhancer adds a :subscribe function to the store which allows you to supply one or more
functions to subscribe to changes to the store. The :subscribe function can optionally take a type-fn which can
filter out which types your subscription is notified about. :subscribe returns a zero arity function that will
cancel the subscription.
(ns my-namespace.core
(:require [com.ben-allred.collaj.core :as collaj]
[com.ben-allred.collaj.enhancers :as collaj.en]))
(def store (collaj/create-store my-reducer collaj.en/with-subscribers))
(def dispatch (:dispatch store))
(def subscribe (:subscribe store))
(subscribe (fn [action] (println "all actions" action)))
(def unsubscribe-ab
(subscribe #{:type-a :type-b}
(fn [action] (println "only types a and b" action))))
(dispatch [:type-a])
;; "all actions [:type-a]"
;; "only-types a and b [:type-a]"
(dispatch [:type-b {:some :data}])
;; "all actions [:type-a {:some :data}]"
;; "only-types a and b [:type-a {:some :data}]"
(dispatch [:type-c])
;; "all actions [:type-c]"
(unsubscribe-ab)
(dispatch [:type-a])
;; "all actions [:type-a]"This enhancer adds a :watch function to the store which takes a path from the root of your state tree and a function to
be invoked when any nested value changes at the supplied path. :watch returns an :unwatch function to be called when
watching the state is no longer needed.
(ns my-namespace.core
(:require [com.ben-allred.collaj.core :as collaj]
[com.ben-allred.collaj.enhancers :as collaj.en]))
(defn my-reducer
([]
{:numbers #{} :letters {:lower-case #{} :upper-case #{}}})
([state [type value]]
(case type
:numbers/add (update state :numbers conj value)
:letters.lower-case/add (update-in state [:letters :lower-case] conj value)
:letters.upper-case/add (update-in state [:letters :upper-case] conj value)
state)))
(def store (collaj/create-store my-reducer collaj.en/with-watchers))
(def dispatch (:dispatch store))
(def watch (:watch store))
(watch [:numbers] (fn [old-value new-value]
(println "numbers are now" new-value)))
(watch [:letters :lower-case] (fn [old-value new-value]
(println "lower-case letters are now" new-value)))
(def unwatch-root
(watch [] (fn [old-value new-value]
(println "entire state was" old-value)
(println "entire state is" new-value))))
(dispatch [:numbers/add 3])
;; numbers are now #{3}
;; entire state was {:numbers #{}, :letters {:lower-case #{}, :upper-case #{}}}
;; entire state is {:numbers #{3}, :letters {:lower-case #{}, :upper-case #{}}}
(dispatch [:letters.lower-case/add "z"])
;; lower-case letters are now #{z}
;; entire state was {:numbers #{3}, :letters {:lower-case #{}, :upper-case #{}}}
;; entire state is {:numbers #{3}, :letters {:lower-case #{z}, :upper-case #{}}}
(dispatch [:letters.upper-case/add "L"])
;; entire state was {:numbers #{3}, :letters {:lower-case #{z}, :upper-case #{}}}
;; entire state is {:numbers #{3}, :letters {:lower-case #{z}, :upper-case #{L}}}
(dispatch [:letters.lower-case/add "z"]) ;; does not change state
(unwatch-root)
(dispatch [:letters.lower-case/add "r"])
;; lower-case letters are now #{z r}This allows you to dispatch keywords and will wrap them into a vector before getting to your reducer.
(ns my-namespace.core
(:require [com.ben-allred.collaj.core :as collaj]
[com.ben-allred.collaj.enhancers :as collaj.en]))
(defn my-reducer
([] nil)
([state action]
action)
(def store (collaj/create-store my-reducer collaj.en/with-keyword-dispatch))
((:dispatch store) :do-something)
((:get-state store))
;; => [:do-something]This allows you to dispatch functions that gets invoked with dispatch and get-state (in a vector).
(ns my-namespace.core
(:require [com.ben-allred.collaj.core :as collaj]
[com.ben-allred.collaj.enhancers :as collaj.en]))
(def store (collaj/create-store (constantly 13) collaj.en/with-fn-dispatch))
((:dispatch store) (fn overflow-the-stack [[dispatch get-state]]
(if (= (get-state) 14)
(dispatch [:didn't-blow-up])
(dispatch overflow-the-stack))))
;; StackOverflowErrorThis allows you to dispatch a core.async/chan. Values placed on the channel will be dispatched as they happen until
the channel is closed.
(ns my-namespace.core
(:require [com.ben-allred.collaj.core :as collaj]
[clojure.core.async :as async]
[com.ben-allred.collaj.enhancers :as collaj.en]))
(def store (collaj/create-store my-reducer 13 collaj.en/with-chan-dispatch))
(def dispatch-chan (async/chan))
((:dispatch store) dispatch-chan)
(async/go
(async/>! dispatch-chan [:some-event {:some :data}])
(async/>! dispatch-chan [:some-othe-event {:some [:other :data]}]))A middleware for "peeking" at the action being dispatched and the resulting state.
(ns my-namespace.core
(:require [com.ben-allred.collaj.core :as collaj]
[com.ben-allred.collaj.enhancers :as collaj.en]))
(def middleware (rdex/with-log-middleware
(comp log/debug (partial str "dispatching action: "))
(comp log/debug (partial str "updated state is: "))
(def store (collaj/create-store my-reducer 13 middleware))You can make your own custom enhancer. An enhancer is a function that accepts the next enhancer in the middleware
chain and returns a function that accepts the reducer and initial-state and returns a store.
(defn do-nothing-enhancer [next]
(fn [reducer initial-state]
(next reducer initial-state)))
(defn hijack-enahncer [next]
(fn [reducer initial-state]
(assoc (next (constantly :muaaahahahaha!) nil) :favorite-number 17)))There is an apply-middleware function in the core namespace that takes one or more dispatch middleware functions and
creates an enhancer to be used when creating your store. A dispatch middleware is a function that accepts the get-state
function and returns a function that accepts the next middleware in the dispatch middleware chain, and returns a new
dispatching function.
(defn do-nothing-dispatch-middleware [get-state]
(fn [next]
(fn [action]
(next action))))
(defn hijack-dispatch-middleware [get-state]
(fn [next]
(fn [action]
(next [:hijacked-action]))))
(def enhancer (collaj/apply-middleware
do-nothing-dispatch-middleware
hijack-dispatch-middleware))There is a create-local-store function which works the same as create-store accept it takes another dispatch
function as the first argument. The resulting store will give you a new dispatch function which will interact with
your local store and also call the supplied dispatch function. Consider using the same middleware for both stores if
you want to be able to dispatch values that are not in the default format.
(def store (collaj/create-store reducer))
(def local-store (:dispatch store) local-reducer)
;; calling (:dispatch local-store) will also call (:dispatch store) with the same valueThere are also a create-custom-store and create-custom-local-store functions which work the same way, except that
they take an additional first argument that will be used to create the ref value that generates the store. If you are
interested in using a custom ref type other than clojure.core/atom, use these functions to create your store.
(def store (collaj/create-custom-store some.other.ref/function reducer initial-state enhancer))
(def sub-store (collaj/create-custom-local-store some.other.ref/function (:dispatch store) reducer))