State Coordination for Rum
Have a simple, re-frame like state management facilities to build web apps with Rum library while leveraging its API.
- Decoupled application state in a single atom
- Reactive queries
- A notion of controller to keep application domains separate
Add to project.clj: [org.roman01la/scrum "0.1.0-SNAPSHOT"]
With Scrum you build everything around a well known architecture pattern in modern SPA development:
DISPATCH EVENT
↓
HANDLE EVENT
↓
QUERY STATE
↓
RENDER
Dispatcher communicates intention to perform an action, whether it is state update or a network request.
(scrum.dispatcher/dispatch! :controller-name :action-name &args)
Controller is a multimethod which performs intended actions against application state. A controller usually have at least an initial state and :init
method.
(def initial-state 0)
(defmulti control (fn [action] action))
(defmethod control :init [action &args db]
(assoc db :counter initial-state))
(scrum.dispatcher/register! :counter control)
A subscription is a reactive query into application state. It is basically an atom which holds a part of the state value. Optional second argument is an aggregate function which computes a materialized view. You can also do parameterized and aggregate subscriptions.
Actual subscription happens in Rum component via rum/reactive
mixin and rum/react
function which hooks in a watch function to update a component when an atom gets an update.
;; normal subscription
(def fname (scrum.core/subscription [:users 0 :fname]))
;; a subscription with aggregate function
(def full-name (scrum.core/subscription [:users 0] #(str (:fname %) " " (:lname %))))
;; parameterized subscription
(defn user [id]
(scrum.core/subscription [:users id]))
;; aggregate subscription
(def discount (scrum.core/subscription [:user :discount]))
(def goods (scrum.core/subscription [:goods :selected]))
(def shopping-cart
(rum/derived-atom [discount goods] ::key
(fn [discount goods]
(let [price (->> goods (map :price) (reduce +))]
(- price (* discount (/ price 100)))))))
;; usage
(rum/defc NameField < rum/reactive []
(let [user (rum/react (user 0))])
[:div
[:div.fname (rum/react fname)]
[:div.lname (:lname user)]
[:div.full-name (rum/react full-name)]
[:div (str "Total: " (rum/react shopping-cart))]])
(ns counter.core
(:require [rum.core :as rum]
[scrum.dispatcher :as d]
[scrum.core :refer [subscription]]))
;;
;; define controller & event handlers
;;
(def initial-state 0)
(defmulti control (fn [action] action))
(defmethod control :init [_ _ db]
(assoc db :counter initial-state))
(defmethod control :inc [_ _ db]
(update db :counter inc))
(defmethod control :dec [_ _ db]
(update db :counter dec))
;;
;; define subscription
;;
(def counter (subscription [:counter]))
;;
;; define UI component
;;
;; create dispatcher for particular controller
(def dispatch-counter! (partial d/dispatch! :counter))
(rum/defc Counter < rum/reactive []
[:div
[:button {:on-click #(dispatch-counter! :dec)} "-"]
[:span (rum/react counter)]
[:button {:on-click #(dispatch-counter! :inc)} "+"]])
;;
;; start up
;;
;; register controller
(d/register! :counter control)
;; initialize registered controllers
(defonce dispatched-init (d/broadcast! :init))
;; render
(rum/mount (Counter)
(. js/document (getElementById "app")))
Check out scrum.router, a minimal routing library for Scrum.
- Get rid of global state
- Storage agnostic architecture? (Atom, DataScript, etc.)
- Better effects handling (network, localStorage, etc.)
Copyright © 2017 Roman Liutikov
Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version.