Created
September 5, 2018 16:21
-
-
Save bensu/0514bf341c9b60c6bd67dc51f0d5a997 to your computer and use it in GitHub Desktop.
rao: rum state management for local components and global app
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(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