Skip to content

Instantly share code, notes, and snippets.

@bensu
Created September 5, 2018 16:21
Show Gist options
  • Save bensu/0514bf341c9b60c6bd67dc51f0d5a997 to your computer and use it in GitHub Desktop.
Save bensu/0514bf341c9b60c6bd67dc51f0d5a997 to your computer and use it in GitHub Desktop.
rao: rum state management for local components and global app
(ns rao.rum
(:require [rum.core :as rum]))
(defn wiree
"Mixin that creates a dispatch, `d!`, that triggers a state transition and updates the component.
On startup (will-mount):
0. It adds an atom rao/local from the value of `initial-state`.
Later whenever `d!` is called it will:
1. update that rao/local state with `step` and
2. call `effect!` to do any side-effects
`initial-state` can be:
- a map with the initial state to the component.
- a function, it will be called with the :rum/args to the component and expected to return the initial state.
`step` is a function from state (as returned originally by `initial-state`) and an event `[action data]` to the next state.
`effect!` is a function from `[previous-state new-state]` and `[action data]` that is called for side-effects after every
state transition"
([initial-state step]
(wire initial-state step nil))
([initial-state step effect!]
{:pre [(or (map? initial-state) (ifn? initial-state))
(ifn? step)
(or (nil? effect!) (ifn? effect!))]}
{:init (cond
(map? initial-state) (fn init [rum-state _]
(assoc rum-state :rao/local (atom initial-state)))
(ifn? initial-state) (fn init [{:keys [rum/args] :as rum-state} _]
(assoc rum-state :rao/local (atom (apply initial-state args))))
:else (throw (ex-info "init-state needs to be either a map or a function" {})))
:will-mount (fn [{:keys [rao/local rum/react-component] :as rum-state}]
#?(:cljs
(add-watch local :rao/local (fn [_ _ old-value new-value]
(when-not (= old-value new-value)
(rum/request-render react-component)))))
(assoc rum-state
:rao/state @local
:rao/d! (fn d! [action data]
(let [state' (swap! local step [action data])]
(when effect!
(effect! state' [action data {:rao/d! d!}]))))))
:before-render (fn [{:keys [rao/local] :as rum-state}]
(assoc rum-state :rao/state @local))}))
;; usage with local state
;; section shows a title and a content
;; when you press on the title, the content is hidden
(defcs section <
(rao.rum/wire (fn [{:keys [default-open?]} _]
(if (some? default-open?)
{:open? default-open?}
{:open? true}))
(fn step [state [action _]]
(case action
:toggle (update state :open? not))))
[{:keys [rao/state rao/d!]}
{:keys [title size default-open?]}
contents]
[:section.section
[:h1 {:class (str "title " (when size (str "is-" size)))}
[:span {:on-click (fn [_] (d! :toggle nil))
:style {:cursor "pointer"}}
(if (:open? state)
"▼"
"▶")
title]]
(when (:open? state)
contents)])
;; usage of the pattern in the larger app:
;; the application's global database
(defonce db (atom {})
;; a function that takes the state of the global database, and an [action data], and returns the next state of the database
(defmulti step (fn [state [action data]] action))
;; a function that takes the states of the state transition, the action that caused it, and does side-effects
;; like fetching from the network or localStorage calls
(defmulti effect! (fn [[prev-state next-state] [action data]] action))
;; in most state transitions, no effects are needed, therefore the default case of `effect!` does nothing
(defmethod effect! :default [[prev-state next-state] [action data]] nil)
(defn d!
"Dispatch an action, causing a state transition for `db` with `step` and side-effects with `effect!`"
[action data]
(let [prev-state @db
next-state (swap! db step [action data])]
(when effect!
(effect! [prev-state next-state] [action data {:rao/d! d!}]))))
;; example usage on a navbar:
(defmethod step :navbar/toggle [state [action data]]
(update state [:navbar :open?] not))
(defmethod step :navbar/navigate [state [action data]]
(assoc state :route (:route data)))
(defmethod effect! :navbar/navigate [[prev-state next-state] [action data]]
(println "navigating to " (:route data))
(set! (.-location js/window) (:route data)))
;; this component can be open or hidden, and it also triggers side-effects
(defc navbar < rum/reactive
[]
(let [state (rum/react db)]
[:nav
[:button {:on-click (fn [_]
(d! :navbar/toggle {}))}
"Hide"]
(when (get-in state [:navbar :open?])
[:ul
[:li
[:a {:on-click (fn [_]
(d! :nav {:route "/"}))}
"Home"]]
[:li
[:a {:on-click (fn [_]
(d! :nav {:route "/about"}))}
"About"]]])]))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment