├── .gitignore ├── LICENSE ├── README.md ├── boot.properties ├── build.boot ├── circle.yml ├── src ├── clj │ └── thdr │ │ └── redux_cljs │ │ └── macros.clj └── cljs │ └── thdr │ └── redux_cljs │ ├── rum.cljs │ └── store.cljs └── test └── cljs └── thdr └── redux_cljs └── store_test.cljs /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | .nrepl-* 3 | .lein-* 4 | .projectile 5 | .#* 6 | .m2 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC 2 | LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM 3 | CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 4 | 5 | 1. DEFINITIONS 6 | 7 | "Contribution" means: 8 | 9 | a) in the case of the initial Contributor, the initial code and 10 | documentation distributed under this Agreement, and 11 | 12 | b) in the case of each subsequent Contributor: 13 | 14 | i) changes to the Program, and 15 | 16 | ii) additions to the Program; 17 | 18 | where such changes and/or additions to the Program originate from and are 19 | distributed by that particular Contributor. A Contribution 'originates' from 20 | a Contributor if it was added to the Program by such Contributor itself or 21 | anyone acting on such Contributor's behalf. Contributions do not include 22 | additions to the Program which: (i) are separate modules of software 23 | distributed in conjunction with the Program under their own license 24 | agreement, and (ii) are not derivative works of the Program. 25 | 26 | "Contributor" means any person or entity that distributes the Program. 27 | 28 | "Licensed Patents" mean patent claims licensable by a Contributor which are 29 | necessarily infringed by the use or sale of its Contribution alone or when 30 | combined with the Program. 31 | 32 | "Program" means the Contributions distributed in accordance with this 33 | Agreement. 34 | 35 | "Recipient" means anyone who receives the Program under this Agreement, 36 | including all Contributors. 37 | 38 | 2. GRANT OF RIGHTS 39 | 40 | a) Subject to the terms of this Agreement, each Contributor hereby grants 41 | Recipient a non-exclusive, worldwide, royalty-free copyright license to 42 | reproduce, prepare derivative works of, publicly display, publicly perform, 43 | distribute and sublicense the Contribution of such Contributor, if any, and 44 | such derivative works, in source code and object code form. 45 | 46 | b) Subject to the terms of this Agreement, each Contributor hereby grants 47 | Recipient a non-exclusive, worldwide, royalty-free patent license under 48 | Licensed Patents to make, use, sell, offer to sell, import and otherwise 49 | transfer the Contribution of such Contributor, if any, in source code and 50 | object code form. This patent license shall apply to the combination of the 51 | Contribution and the Program if, at the time the Contribution is added by the 52 | Contributor, such addition of the Contribution causes such combination to be 53 | covered by the Licensed Patents. The patent license shall not apply to any 54 | other combinations which include the Contribution. No hardware per se is 55 | licensed hereunder. 56 | 57 | c) Recipient understands that although each Contributor grants the licenses 58 | to its Contributions set forth herein, no assurances are provided by any 59 | Contributor that the Program does not infringe the patent or other 60 | intellectual property rights of any other entity. Each Contributor disclaims 61 | any liability to Recipient for claims brought by any other entity based on 62 | infringement of intellectual property rights or otherwise. As a condition to 63 | exercising the rights and licenses granted hereunder, each Recipient hereby 64 | assumes sole responsibility to secure any other intellectual property rights 65 | needed, if any. For example, if a third party patent license is required to 66 | allow Recipient to distribute the Program, it is Recipient's responsibility 67 | to acquire that license before distributing the Program. 68 | 69 | d) Each Contributor represents that to its knowledge it has sufficient 70 | copyright rights in its Contribution, if any, to grant the copyright license 71 | set forth in this Agreement. 72 | 73 | 3. REQUIREMENTS 74 | 75 | A Contributor may choose to distribute the Program in object code form under 76 | its own license agreement, provided that: 77 | 78 | a) it complies with the terms and conditions of this Agreement; and 79 | 80 | b) its license agreement: 81 | 82 | i) effectively disclaims on behalf of all Contributors all warranties and 83 | conditions, express and implied, including warranties or conditions of title 84 | and non-infringement, and implied warranties or conditions of merchantability 85 | and fitness for a particular purpose; 86 | 87 | ii) effectively excludes on behalf of all Contributors all liability for 88 | damages, including direct, indirect, special, incidental and consequential 89 | damages, such as lost profits; 90 | 91 | iii) states that any provisions which differ from this Agreement are offered 92 | by that Contributor alone and not by any other party; and 93 | 94 | iv) states that source code for the Program is available from such 95 | Contributor, and informs licensees how to obtain it in a reasonable manner on 96 | or through a medium customarily used for software exchange. 97 | 98 | When the Program is made available in source code form: 99 | 100 | a) it must be made available under this Agreement; and 101 | 102 | b) a copy of this Agreement must be included with each copy of the Program. 103 | 104 | Contributors may not remove or alter any copyright notices contained within 105 | the Program. 106 | 107 | Each Contributor must identify itself as the originator of its Contribution, 108 | if any, in a manner that reasonably allows subsequent Recipients to identify 109 | the originator of the Contribution. 110 | 111 | 4. COMMERCIAL DISTRIBUTION 112 | 113 | Commercial distributors of software may accept certain responsibilities with 114 | respect to end users, business partners and the like. While this license is 115 | intended to facilitate the commercial use of the Program, the Contributor who 116 | includes the Program in a commercial product offering should do so in a 117 | manner which does not create potential liability for other Contributors. 118 | Therefore, if a Contributor includes the Program in a commercial product 119 | offering, such Contributor ("Commercial Contributor") hereby agrees to defend 120 | and indemnify every other Contributor ("Indemnified Contributor") against any 121 | losses, damages and costs (collectively "Losses") arising from claims, 122 | lawsuits and other legal actions brought by a third party against the 123 | Indemnified Contributor to the extent caused by the acts or omissions of such 124 | Commercial Contributor in connection with its distribution of the Program in 125 | a commercial product offering. The obligations in this section do not apply 126 | to any claims or Losses relating to any actual or alleged intellectual 127 | property infringement. In order to qualify, an Indemnified Contributor must: 128 | a) promptly notify the Commercial Contributor in writing of such claim, and 129 | b) allow the Commercial Contributor tocontrol, and cooperate with the 130 | Commercial Contributor in, the defense and any related settlement 131 | negotiations. The Indemnified Contributor may participate in any such claim 132 | at its own expense. 133 | 134 | For example, a Contributor might include the Program in a commercial product 135 | offering, Product X. That Contributor is then a Commercial Contributor. If 136 | that Commercial Contributor then makes performance claims, or offers 137 | warranties related to Product X, those performance claims and warranties are 138 | such Commercial Contributor's responsibility alone. Under this section, the 139 | Commercial Contributor would have to defend claims against the other 140 | Contributors related to those performance claims and warranties, and if a 141 | court requires any other Contributor to pay any damages as a result, the 142 | Commercial Contributor must pay those damages. 143 | 144 | 5. NO WARRANTY 145 | 146 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON 147 | AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER 148 | EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR 149 | CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A 150 | PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the 151 | appropriateness of using and distributing the Program and assumes all risks 152 | associated with its exercise of rights under this Agreement , including but 153 | not limited to the risks and costs of program errors, compliance with 154 | applicable laws, damage to or loss of data, programs or equipment, and 155 | unavailability or interruption of operations. 156 | 157 | 6. DISCLAIMER OF LIABILITY 158 | 159 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY 160 | CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, 161 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION 162 | LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 163 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 164 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE 165 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY 166 | OF SUCH DAMAGES. 167 | 168 | 7. GENERAL 169 | 170 | If any provision of this Agreement is invalid or unenforceable under 171 | applicable law, it shall not affect the validity or enforceability of the 172 | remainder of the terms of this Agreement, and without further action by the 173 | parties hereto, such provision shall be reformed to the minimum extent 174 | necessary to make such provision valid and enforceable. 175 | 176 | If Recipient institutes patent litigation against any entity (including a 177 | cross-claim or counterclaim in a lawsuit) alleging that the Program itself 178 | (excluding combinations of the Program with other software or hardware) 179 | infringes such Recipient's patent(s), then such Recipient's rights granted 180 | under Section 2(b) shall terminate as of the date such litigation is filed. 181 | 182 | All Recipient's rights under this Agreement shall terminate if it fails to 183 | comply with any of the material terms or conditions of this Agreement and 184 | does not cure such failure in a reasonable period of time after becoming 185 | aware of such noncompliance. If all Recipient's rights under this Agreement 186 | terminate, Recipient agrees to cease use and distribution of the Program as 187 | soon as reasonably practicable. However, Recipient's obligations under this 188 | Agreement and any licenses granted by Recipient relating to the Program shall 189 | continue and survive. 190 | 191 | Everyone is permitted to copy and distribute copies of this Agreement, but in 192 | order to avoid inconsistency the Agreement is copyrighted and may only be 193 | modified in the following manner. The Agreement Steward reserves the right to 194 | publish new versions (including revisions) of this Agreement from time to 195 | time. No one other than the Agreement Steward has the right to modify this 196 | Agreement. The Eclipse Foundation is the initial Agreement Steward. The 197 | Eclipse Foundation may assign the responsibility to serve as the Agreement 198 | Steward to a suitable separate entity. Each new version of the Agreement will 199 | be given a distinguishing version number. The Program (including 200 | Contributions) may always be distributed subject to the version of the 201 | Agreement under which it was received. In addition, after a new version of 202 | the Agreement is published, Contributor may elect to distribute the Program 203 | (including its Contributions) under the new version. Except as expressly 204 | stated in Sections 2(a) and 2(b) above, Recipient receives no rights or 205 | licenses to the intellectual property of any Contributor under this 206 | Agreement, whether expressly, by implication, estoppel or otherwise. All 207 | rights in the Program not expressly granted under this Agreement are 208 | reserved. 209 | 210 | This Agreement is governed by the laws of the State of Washington and the 211 | intellectual property laws of the United States of America. No party to this 212 | Agreement will bring a legal action under this Agreement more than one year 213 | after the cause of action arose. Each party waives its rights to a jury trial 214 | in any resulting litigation. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Redux for ClojureScript based on core.async and transducers. 2 | [![Clojars Project](https://img.shields.io/clojars/v/io.thdr/redux-cljs.svg)](https://clojars.org/io.thdr/redux-cljs) [![Circle CI](https://circleci.com/gh/konukhov/redux-cljs/tree/master.svg?style=shield)](https://circleci.com/gh/konukhov/redux-cljs/tree/master) 3 | 4 | Functional state management for reactive apps. 5 | 6 | It's highly recommended to read [Redux overview](https://github.com/reactjs/redux) first. 7 | 8 | ## Differences from pure Redux 9 | 10 | + [Redux-thunk](https://github.com/gaearon/redux-thunk) is already part of redux-cljs. 11 | + Slightly different reducer's type signature. 12 | + No middlewares and enhancers yet. I'm thinking of adding extensibility in next releases. 13 | 14 | ## Usage 15 | 16 | Add this dependency to your project.clj or build.boot: 17 | 18 | ```clojure 19 | [io.thdr/redux-cljs "0.1.0-SNAPSHOT"] 20 | ``` 21 | 22 | Version 0.1.0 will be available as soon as I write some docs and add more examples. 23 | 24 | ### Reducers 25 | 26 | #### Creating reducers 27 | 28 | Reducer must be a pure function of `action` which returns a function (also pure) of `state`. 29 | 30 | ```clojure 31 | (def reducer 32 | (fn [action] 33 | (fn [state] 34 | (case (:type action) 35 | :inc-counter (update-in state [:counter] inc) 36 | :dec-counter (update-in state [:counter] dec) 37 | :reset-counter (assoc state [:counter] (:counter action)) 38 | state)))) 39 | ``` 40 | 41 | There is a macro which makes adding reducers easier: 42 | 43 | ```clojure 44 | (require [thdr.redux-cljs.macros :as r]) 45 | 46 | (r/defreducer reducer [state data] 47 | :inc (update-in state [:counter] inc) 48 | :dec (update-in state [:counter] dec) 49 | :reset (assoc state [:counter] (:counter data))) 50 | ``` 51 | 52 | #### Combining reducers 53 | 54 | Two or more reducers can be combined to one. 55 | 56 | ```clojure 57 | (require [thdr.redux-cljs.store :refer [combine-reducers]]) 58 | 59 | (defreducer first-reducer [state _] 60 | :first (assoc state :first "first")) 61 | 62 | (defreducer second-reducer [state _] 63 | :second (assoc state :second "second")) 64 | 65 | (def reducer ;; which can be passed to `create-store` 66 | (combine-reducers first-reducer second-reducer)) 67 | ``` 68 | 69 | ### Actions 70 | 71 | Action should be a map with `:type` key or a function of `state` (see [redux-thunk](https://github.com/gaearon/redux-thunk))). 72 | 73 | ```clojure 74 | (def inc-action {:type :inc}) 75 | (def dec-action {:type :dec}) 76 | 77 | (defn reset-action [value] 78 | {:type :reset 79 | :counter value}) 80 | 81 | (require [thdr.redux-cljs.store :refer [dispatch]]) ;; see below 82 | 83 | ;; useful for http requests and other stuff 84 | (def thunk-action 85 | (fn [state] 86 | (dispatch state inc-action) 87 | (js/setTimeout (clj->js #(dispatch state dec-action)) 1000)) 88 | ``` 89 | 90 | ### Stores 91 | 92 | ```clojure 93 | (require [thdr.redux-cljs :refer [create-store 94 | subscribe 95 | unsubscribe 96 | dispatch 97 | get-state]]) 98 | 99 | (def initial-state 100 | {:counter 0}) 101 | 102 | ;; After store is created it must subscribe to actions stream. 103 | ;; It is also possible to create a store with empty initial state: 104 | ;; (create-store reducer) 105 | (def store 106 | (-> (create-store initial-state reducer) 107 | (subscribe))) 108 | 109 | (dispatch store inc-action) ;; => state == {:counter 1} 110 | (dispatch store dec-action) ;; => state == {:counter 0} 111 | (dispatch store (reset-action 5) ;; => state == {:counter 5} 112 | 113 | (dispatch store ;; => state == {:counter 6} ... and after 1 second 114 | thunk-action ;; state == {:counter 5} 115 | 116 | (unsubscribe store) ;; closes core.async chanels 117 | ``` 118 | 119 | ## Compatibility with ClojureScript React-based libraries 120 | 121 | There's currently an adapter for Rum only but it should be trivial to use cljs-redux with any react-based library based on atoms. Don't forget to call `unsubscribe` on store in `componentWillUnmount` in order to prevent memory leaks. 122 | 123 | ### Rum example 124 | 125 | ```clojure 126 | (ns rum-example.core 127 | (:require [thdr.redux-cljs.rum :refer [redux-store]] 128 | [thdr.redux-cljs.store :refer [dispatch get-state] :as store] 129 | [rum.core :as rum]) 130 | (:require-macros [thdr.redux-cljs.macros :refer [defreducer]])) 131 | 132 | (def initial-state {:counter 0}) 133 | 134 | (def inc-action {:type :inc}) 135 | (def dec-action {:type :dec}) 136 | 137 | (defreducer reducer [state data] 138 | :inc (update-in state [:counter] inc) 139 | :dec (update-in state [:counter] dec)) 140 | 141 | (rum/defc counter-component < rum/cursored rum/cursored-watch [counter] 142 | [:p (str "Counter is: " counter)]) 143 | 144 | (rum/defcs test-page < (redux-store initial-state reducer) [rum-state] 145 | (let [store (::store/store rum-state) 146 | counter (rum/cursor (get-state store [:counter]))] 147 | [:.page 148 | (counter-component counter) 149 | [:.controls 150 | [:span {:on-click #(dispatch store inc-action)} "+"] 151 | [:span {:on-click #(dispatch store dec-action)} "-"]]])) 152 | 153 | (rum/mount (test-page) js/document.body) 154 | ``` 155 | 156 | ### Reagent (not tested yet) 157 | 158 | To use redux-cljs with Reagent you should tell `create-store` to use `ratom` instead of plain Clojure `atom`: 159 | 160 | ```clojure 161 | (require [reagent.core :as r]) 162 | 163 | ... 164 | 165 | (def store (create-store initial-state reducer :atom-fn #'r/atom)) 166 | ``` 167 | 168 | ## To-do 169 | 170 | + Validate actions with `plumatic/schema` 171 | + Add examples 172 | + Write docstrings :) 173 | + Test reagent 174 | + Maybe add middlewares/enhances. 175 | 176 | 177 | ## License 178 | 179 | Copyright © 2016 Theodore Konukhov 180 | 181 | Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version. 182 | -------------------------------------------------------------------------------- /boot.properties: -------------------------------------------------------------------------------- 1 | BOOT_EMIT_TARGET=no -------------------------------------------------------------------------------- /build.boot: -------------------------------------------------------------------------------- 1 | (set-env! :resource-paths #{"src/clj" "src/cljs"} 2 | :dependencies '[[org.clojure/clojurescript "1.8.40" :scope "provided"] 3 | [org.clojure/core.async "0.2.374" :scope "provided"] 4 | [boot-codox "0.9.5" :scope "test"] 5 | [adzerk/bootlaces "0.1.13" :scope "test"] 6 | [rum "0.8.1" :scope "test"] 7 | [reagent "0.6.0-alpha" :scope "test"] 8 | [crisptrutski/boot-cljs-test "0.2.2-SNAPSHOT" 9 | :scope "test"]]) 10 | 11 | (require '[crisptrutski.boot-cljs-test :refer [test-cljs]] 12 | '[adzerk.bootlaces :refer :all] 13 | '[codox.boot :refer [codox]]) 14 | 15 | (def +version+ "0.1.0-SNAPSHOT") 16 | (bootlaces! +version+ :dont-modify-paths? true) 17 | 18 | (task-options! 19 | pom {:project 'io.thdr/redux-cljs 20 | :version +version+ 21 | :description "Redux implementation in ClojureScript." 22 | :url "https://github.com/konukhov/redux-cljs" 23 | :scm {:url "https://github.com/konukhov/redux-cljs"} 24 | :license {"Eclipse Public License" 25 | "http://www.eclipse.org/legal/epl-v10.html"}}) 26 | 27 | (deftask with-test-paths [] 28 | (merge-env! :source-paths #{"test/cljs"}) 29 | identity) 30 | 31 | (deftask test [] 32 | (comp (with-test-paths) 33 | (test-cljs))) 34 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | java: 3 | version: oraclejdk8 4 | 5 | dependencies: 6 | cache_directories: 7 | - ~/.boot 8 | - ~/.m2 9 | pre: 10 | - wget https://github.com/boot-clj/boot-bin/releases/download/latest/boot.sh 11 | - mv boot.sh boot && chmod a+x boot && sudo mv boot /usr/local/bin 12 | override: 13 | - boot -u 14 | 15 | test: 16 | override: 17 | - boot test 18 | -------------------------------------------------------------------------------- /src/clj/thdr/redux_cljs/macros.clj: -------------------------------------------------------------------------------- 1 | (ns thdr.redux-cljs.macros) 2 | 3 | (defmacro defreducer 4 | "Define Redux-like reducer" 5 | [name bindings & matches] 6 | {:pre [#(vector? bindings) 7 | #(= (2 (count bindings)))]} 8 | (let [[state data] bindings] 9 | `(def ~name 10 | (fn [action#] 11 | (fn [~state] 12 | (let [~data (dissoc action# :type)] 13 | (case (:type action#) 14 | ~@matches 15 | ~state))))))) 16 | 17 | (defmacro defaction 18 | ([name action-data] (defaction name nil action-data)) 19 | ([name bindings action-data] 20 | {:pre [(associative? action-data)]} 21 | `(let [body# (merge ~action-data 22 | {:type ~(keyword name)})] 23 | (def ~name 24 | (if (nil? ~bindings) 25 | body# 26 | (fn ~bindings 27 | body#)))))) 28 | 29 | (defmacro defactionfn [& args] 30 | `(defn ~@args)) 31 | 32 | (comment 33 | (defreducer inc-reducer [state data] 34 | :inc (update-in state [:counter] inc) 35 | :dec (update-in state [:counter] dec) 36 | :reset (assoc state :counter (:counter data)))) 37 | -------------------------------------------------------------------------------- /src/cljs/thdr/redux_cljs/rum.cljs: -------------------------------------------------------------------------------- 1 | (ns thdr.redux-cljs.rum 2 | "This namespace provides mixin and some 3 | other helpers for compatibility with Rum." 4 | (:require [thdr.redux-cljs.store :refer [create-store 5 | get-state 6 | subscribe 7 | unsubscribe] :as store])) 8 | 9 | (def default-context-key ::store/state-atom) 10 | (def default-store-key ::store/store) 11 | 12 | (defn- context-types-map 13 | [context-types-key context-key] 14 | {:class-properties {context-types-key 15 | (clj->js 16 | {context-key (js/React.PropTypes.instanceOf Atom)})}}) 17 | 18 | (defn- child-context-types [key] 19 | (context-types-map :childContextTypes key)) 20 | 21 | (defn context-types 22 | "Helper method which provides initial Rum mixin 23 | for dealing with child contexts in Rum components." 24 | ([] (context-types default-context-key)) 25 | ([key] (context-types-map :contextTypes key))) 26 | 27 | (defn context->redux-state 28 | ([comp-state] (context->redux-state comp-state default-context-key)) 29 | ([comp-state key] 30 | (-> comp-state 31 | :rum/react-component 32 | .-context 33 | (js->clj) 34 | (get (name key))))) 35 | 36 | (defn redux-store 37 | "Creates mixin which puts store object into Rum-component's map. 38 | 39 | Options: 40 | 41 | *key*: Key with which store will be associated in a component's map. 42 | 43 | *with-child-context?*: Adds `getChildContext` and `childContextTypes` 44 | 45 | *context-key*: Key with which redux state will be associated in 46 | React's context map. Ignored if `with-child-context?` is set to false." 47 | [initial-state reducer & {:keys [key context-key with-child-context?] 48 | :or {key default-store-key 49 | context-key default-context-key 50 | with-child-context? false}}] 51 | (let [store (create-store initial-state reducer) 52 | mixin {:transfer-state 53 | (fn [old new] 54 | (assoc new key (old key))) 55 | 56 | :will-mount 57 | (fn [state] 58 | (assoc state key (subscribe store))) 59 | 60 | :will-unmount 61 | (fn [state] 62 | (let [store (-> key state unsubscribe)] 63 | (assoc state key nil)))}] 64 | (if with-child-context? 65 | (merge mixin 66 | {:child-context (fn [_] {context-key (get-state store)})} 67 | (child-context-types context-key)) 68 | mixin))) 69 | -------------------------------------------------------------------------------- /src/cljs/thdr/redux_cljs/store.cljs: -------------------------------------------------------------------------------- 1 | (ns thdr.redux-cljs.store 2 | "Main API for creating and using Redux-cljs" 3 | (:require [cljs.core.async :as async :refer [!]]) 4 | (:require-macros [cljs.core.async.macros :refer [go go-loop]])) 5 | 6 | (defprotocol IReduxStore 7 | (get-state [this] 8 | "Returns a state atom") 9 | 10 | (subscribe [this] 11 | "Creates a go-loop which reacts to actions stream. 12 | Must be called after store is created.") 13 | 14 | (unsubscribe [this] 15 | "Closes core.async channels. It's recommended to 16 | call `unsubscribe` in `will-unmount` React handlers 17 | in order to prevent memory leaks.") 18 | 19 | (dispatch [this action] 20 | "Puts an action (see [thdr.redux-cljs.macros/defaction]) 21 | to main event loop. Action should be either a hash-map 22 | with required :type key or a function of store.") 23 | 24 | (replace-reducer [this next-reducer] 25 | "Replaces reducer passed in [create-store]")) 26 | 27 | (defn- make-event-bus 28 | [reducer] 29 | (async/chan 1 (map reducer))) 30 | 31 | (defrecord Store [state bus event-loop] 32 | IReduxStore 33 | 34 | (get-state [_] 35 | state) 36 | 37 | (subscribe [this] 38 | (if-not event-loop 39 | (let [event-loop (go-loop [] 40 | (when-let [state-fn (! bus action))))) 59 | 60 | (replace-reducer [this next-reducer] 61 | (-> this 62 | unsubscribe 63 | (assoc :bus (make-event-bus next-reducer)) 64 | subscribe))) 65 | 66 | (defn- atom? [obj] 67 | (satisfies? IAtom obj)) 68 | 69 | (defn- make-state-atom [initial-state atom-fn] 70 | {:pre [(or (associative? initial-state) 71 | (atom? initial-state))]} 72 | (if (atom? initial-state) 73 | initial-state 74 | (let [state (atom-fn initial-state)] 75 | (if (atom? state) 76 | state 77 | (throw (js/Error. (str "You are trying to pass as :atom-fn " 78 | "something that doesn't make an atom."))))))) 79 | 80 | (defn create-store 81 | "Creates a redux-store. 82 | 83 | **initial-state** should be either an associative 84 | data structure or an atom (which could be useful 85 | in development environments with live-reloading 86 | but not recommended in production environments). 87 | 88 | **reducer** is a function which matches action types 89 | and returns a function of state which then updates 90 | state atom via `swap!`. 91 | 92 | It should look like this: 93 | 94 | ``` 95 | (fn [state] 96 | (fn [action] 97 | (case (:type action) 98 | ;; match action and update state 99 | state 100 | ``` 101 | 102 | There is also a macro for that: [thdr.redux-cljs.macros/defreducer]. 103 | 104 | Options: 105 | 106 | *atom-fn*: provide a custom function for creating state atom. 107 | For example, if you're using Reagent, you might want the state to be 108 | held in RAtom instead of plain Clojure atom." 109 | ([reducer] (create-store {} reducer)) 110 | ([initial-state reducer & {:keys [atom-fn] :or {atom-fn #'atom}}] 111 | (let [state (make-state-atom initial-state atom-fn) 112 | bus (make-event-bus reducer)] 113 | (map->Store {:state state :bus bus})))) 114 | 115 | (defn combine-reducers [& reducers] 116 | (fn [action] 117 | (apply comp (map #(% action) reducers)))) 118 | -------------------------------------------------------------------------------- /test/cljs/thdr/redux_cljs/store_test.cljs: -------------------------------------------------------------------------------- 1 | (ns thdr.redux-cljs.store-test 2 | (:require-macros [cljs.test :refer [deftest testing is async]] 3 | [thdr.redux-cljs.macros :refer [defreducer]] 4 | [cljs.core.async.macros :refer [go]]) 5 | (:require [cljs.test] 6 | [cljs.core.async :refer [! chan close!]] 7 | [reagent.core :as r] 8 | [thdr.redux-cljs.store :as s])) 9 | 10 | ;;;;;;;;;; testing data ;;;;;;;;;; 11 | 12 | (def initial-state {:counter 0}) 13 | 14 | (defn reset-action 15 | [counter] 16 | {:type :reset 17 | :counter counter}) 18 | 19 | (def inc-action {:type :inc}) 20 | (def dec-action {:type :dec}) 21 | 22 | (def add-stuff-action 23 | {:type :add-stuff 24 | :stuff "stuff"}) 25 | 26 | (def thunk-action 27 | (fn [state] 28 | (dotimes [_ 2] (s/dispatch state inc-action)) 29 | (s/dispatch state dec-action))) 30 | 31 | (def do-a-action {:type :do-a}) 32 | (def do-b-action {:type :do-b}) 33 | 34 | (defreducer test-reducer [state data] 35 | :inc (update-in state [:counter] inc) 36 | :dec (update-in state [:counter] dec) 37 | :reset (assoc state :counter (:counter data))) 38 | 39 | (defreducer test-replace-reducer [state data] 40 | :add-stuff (assoc state :stuff (:stuff data))) 41 | 42 | (defreducer test-reducer-a [state _] 43 | :do-a (assoc state :a "a")) 44 | 45 | (defreducer test-reducer-b [state _] 46 | :do-b (assoc state :b "b")) 47 | 48 | ;;;;;;;;;; tests ;;;;;;;;;; 49 | 50 | (deftest store-test 51 | (let [store (s/create-store initial-state test-reducer)] 52 | (testing "create store" 53 | (is (satisfies? s/IReduxStore store)) 54 | (is (instance? s/Store store)) 55 | (is (nil? (:event-loop store)))) 56 | 57 | (testing "get state" 58 | (is (= initial-state @(s/get-state store)))))) 59 | 60 | (deftest initial-state-is-an-atom 61 | (testing "clojure atom" 62 | (let [state (atom {}) 63 | store (s/create-store state test-reducer)] 64 | (is (= state (s/get-state store))))) 65 | 66 | (testing "reagent atom" 67 | (let [state (r/atom {}) 68 | store (s/create-store state test-reducer)] 69 | (is (= state (s/get-state store))))) 70 | 71 | (testing ":atom-fn atom" 72 | (let [store (s/create-store initial-state test-reducer :atom-fn atom) 73 | state (s/get-state store)] 74 | (is (instance? Atom state)) 75 | (is (= initial-state @state)))) 76 | 77 | (testing ":atom-fn ratom" 78 | (let [store (s/create-store initial-state test-reducer :atom-fn r/atom) 79 | state (s/get-state store)] 80 | (is (instance? reagent.ratom.RAtom state)) 81 | (is (= initial-state @state))))) 82 | 83 | (deftest dispatch-test 84 | (let [store (s/subscribe (s/create-store initial-state test-reducer)) 85 | state (s/get-state store) 86 | chan (chan)] 87 | (async done 88 | (add-watch state :dispatch-test (fn [_ _ _ new-state] 89 | (go (>! chan new-state)))) 90 | 91 | (go 92 | (s/dispatch store inc-action) 93 | (is (= 1 (:counter (! chan new-state)))) 113 | 114 | (go 115 | (s/dispatch store thunk-action) 116 | 117 | (is (= 1 (:counter ( (s/create-store initial-state test-reducer) 128 | (s/subscribe) 129 | (s/replace-reducer test-replace-reducer)) 130 | state (s/get-state store) 131 | chan (chan)] 132 | 133 | (async done 134 | (add-watch state :replace-reducer-test (fn [_ _ _ new-state] 135 | (go (>! chan new-state)))) 136 | 137 | (go 138 | (s/dispatch store add-stuff-action) 139 | (is (= "stuff" (:stuff (! chan new-state)))) 154 | 155 | (go 156 | (s/dispatch store do-a-action) 157 | (is (= "a" (:a (