Extending Readux
This document assumes familiarity with what's written intended the design overview and reference pages, which explain the rationale and introduces core terminology.
Of libraries and frameworks
readux, like redux, is a fairly unopinionated library. Libraries are limited in scope whereas frameworks are cohesive, thoughtful, but prescriptive solutions to a set of problems. Frameworks are a great thing when your use-case aligns perfectly with what the authors intended - less so when you're fighting them to achieve something not envisioned or catered to.
Conversely, libraries are concerned with solving a relatively small problem and you're left to assemble the libraries needed to solve your problem. The upside is that you can tailor a solution matching the problem.
For example, the Redux community has multiple takes on managing async requests, from thunks to promises through generators. There is no universal, right () solution - the best approach is relative to the scale and complexity of your application. Even if there were, using a set of libraries means that once some superior approach to solving the problem comes along, you can swap out a library instead of starting all over in a new framework.
In summary, readux doesn't do much, but it's designed to play well with other libraries, and that's a good thing().
How to extend readux
In concrete terms, readux allows writing store enhancers augment the store, adding additional functionality like 'queries', debuggers etc and middleware, which sits between the call dispatching an action and it being forwarded to the reducer. Generally, if you mean to simply alter actions on a case-by-case (pure) basis, middleware will do. If you need to retain some state, e.g. to store a map of registered queries or data related to a debugger, then writing a store enhancer is the way to go.
Middleware
Middleware is used whenever you wish to automatically do something before an action reaches your store's root reducer or before the resulting model is persisted.
Example: Handling Promises
Say we wanted to automatically process incoming actions which submits a
promise as their argument, splitting the action :fetch-posts
into
:fetch-posts.rq
, :fetch-posts.success
and :fetch-posts.error
to signify
when the request is made, and when it ends in a success or an error,
respectively. We could wrap this process ourselves for each async request our
application would make, or write a piece of middleware to handle promises for us.
Incidentially, if you wish to do this, or already use promesa, try readux-promesa.
Example: Verifying app model with cljs.spec
Similarly, if you wished to ensure that for each action, the model retained some specific structure, you could implement a middleware function to validate the resulting model against a clojure.spec schema.
Writing middleware
Signature: next -> model, action -> new-model
We see from the signature that middleware takes a single argument 'next
', which
is the middleware/reducer function to execute next. From this, we get a function,
which, when given a 'model
' and an 'action
', yields a new model which is the
result of applying the action to the old model.
;; This middleware does nothing but pass the action along.
(defn passthrough-middleware
[next]
(fn [model action]
(apply next model action)))
Often times, you only need to do something to an action or in response to an action before it is received by the reducer, but sometimes it can be handy to work on the resulting model too.
Readux ships with a middleware function, log-model-diff
which can show you
every action that is dispatched and how it changed the model.
log-model-diff
stores a copy of the model before passing it on to the next
middleware function in the chain, ideally the reducer itself, comparing the
resulting model to the initial model.
Try reading its source
for inspiration on writing your own middleware functions.
Remember, middleware functions can:
- prematurely abort processing simply by not calling the next function in line.
- alter/replace and dispatch additional actions.
- (see readux-promesa which uses this to handle promises)
- alter/replace the input model before passing it on to the next function
- alter/replace the new model before returning it to the caller (ultimately the
dispatch
function, which stores the new model)
Using middleware
To use middleware, we supply store
with an optional second argument
which modifies the construction of the store somehow before it is
returned to us.
In this case, using apply-mw
allows us to install an arbitrary sequence
of functions in between dispatch
which first receives and action, and
the store's reducer function. Here's an example:
(require '[readux.core :as rdc])
(require '[readux.store :as rds])
;; ...
(defonce store (rdc/store app-reducer (rds/apply-mw some-other-middleware log-model-diff)))
In this case, the action will go through the system like so:
dispatch
this is when the action is first dispatchedsome-other-middleware
log_model_diff
app-reducer
- the application's reducer function.
Store enhancers
Signature: store -> store
Store enhancers, as their name implies, modify the store object itself. Generally, store enhancers should be used when middleware won't suffice, such as when new functionality relying on additional fields in the store object is implemented.
Example: readux-debugger
For example, the readux-debugger creates an additional store, tied to the app-store which controls its own model and which has its own reducer. By enhancing the store, it can replace the standard dispatch-function invoked when an action is dispatched. The dispatch function normally finds the store's reducer, and passes along the action, storing the value returned as the new model. By replacing the dispatcher, the readux-debugger can intercept and assure that debug-related actions never reach the app reducer while ensuring app-related actions are dispatched to both the debugger and app stores.
(NOTE not presently released, coming soon)
Writing Store Enhancers
The smallest possible example is also a bit silly, typically you want to wrap the :dispatch
function or add some
additional entries to facilitate some expanded interface.
(defn make-stores-great-again
[app-store]
(swap! app-store assoc :is-great? true))
With our additional data, we can implement new store functions, such as this:
(defn great-store?
[store]
(get @store :is-great? false))
The ability for a store to use middleware functions is actually implemented as
a store enhancer, see apply-mw
in readux store.cljs.
Using Store Enhancers
The store function accepts an additional argument, allowing you to specify an enhancer function. To use multiple store enhancers, use composition.
(def s (store my-reducer make-stores-great-again))
(great-store? s)
;;=> true