[funcool/potok "2.7.0"]
Potok is a tiny (100LOC) reactive streams based state management toolkit for ClojureScript.
Potok lies on top of two concepts: events and reactive store.
The events are entities that your application will emit in order to send data or
action to the store. They will be emitted using potok.core/emit!
function.
There are three types of events:
update: that represents a synchronous state transformation.
watch: that represents an asynchronous operation.
effect: that represents a side effectful operation.
Let’s see a detailed explanation of each event type:
The update event represents the simple synchronous state
transformation. It just consists of a type defined using defrecord,
implementing a UpdateEvent
protocol.
The update
function receives the current state as argument and
should return the transformed state. Let’s see an example:
(require '[potok.core :as ptk])
(defrecord Increment []
ptk/UpdateEvent
(update [_ state]
(update state :counter (fnil inc 0))))
You may be thinking, the signature of the update
function is very
similar to a reduce function. And in fact, it does just that the state
reduction, and it can be defined using a plain ClojureScript function:
(defn increment
[state]
(update state :counter (fnil inc 0))
Although it is very simple to define this kind of events as functions, the type based events are more recommended, especially when you want to pass arguments to the event. Let’s see an example:
(defrecord IncrementBy [n]
ptk/UpdateEvent
(update [_ state]
(update state :counter + n)))
The same event would look much uglier if defined using function syntax:
(defn increment-by
[n]
(fn [state]
(update state :counter + n)))
Apart from the simple state transformations, applications usually need to perform asynchronous operations such as call remote API, access local database, etc. This is where the watch events play their role. They are designed to handle asynchronous operations.
Let’s see how it looks:
(require '[beicon.core :as rx])
(defrecord DelayedIncrement []
ptk/WatchEvent
(watch [_ state stream]
(->> (rx/just (->Increment)) ; create a instance of `Increment` event
(rx/delay 100)))) ; delay the stream for 100ms
The responsibility of the watch
function is to perform an
asynchronous operation and return a stream of one or more events. In
the example, you can observe, that it just returns a stream of one
Increment
event instance delayed 100 milliseconds (thus emulating
some latency).
That stream will be re-injected into the main stream and those events
will be processed in the same way as if you emitted them with
potok.core/emit!
function.
The additional stream
parameter to the watch
function represents
the main stream where all events will arrive, so you can build logic
needed for a synchronization with other events or just a handling of
some kind of a cancellation.
The effect event represents a side effectfull action. In the same way as the watch event, it receives the current state and the main stream as arguments.
Let’s see how it look:
(defrecord Notify [title message]
ptk/EffectEvent
(effect [_ state stream]
(let [params #js {:body message}]
(js/Notification. title params))))
The return value of the effect
function is completely ignored.
In the previous section we have seen events, the store is the object that processes them. It has the following responsibilities:
Hold the application state.
Process incoming events.
Emit the changes using reactive streams.
In the contrast to other similar approaches to implementing store (such that re-frame or redux), this approach does not allow to access the state directly, you only can watch it and materialize it to some reference type like ClojureScript atom. This ensures that the state can only be transformed using events.
To create store you just need to execute the potok.core/store
function:
(def store (ptk/store))
If no arguments is passed to store
function, the initial state is
initialized as nil
. This is how you can provide an initial state:
(def store (ptk/store {:state {:counter 0}}))
The store
object from the user perspective is a reactive stream that
emits the state each time it is transformed.
Internally it is implemented using BehaviorSubject and each new subscription always receives the latest state object followed by state objects transformed by events.
In order to be able to access the state, we need to materialize it. A good approach is using just an atom to hold the materialized state:
(defonce state-view
(rx/to-atom store))
Now that we have created a store, and a materialized view of the state. Let’s start to emit events:
(ptk/emit! store (->Increment))
Now if you observe the state dereferencing the state-view
atom, you
will see it transformed:
@state-view
;; => {:counter 1}
In many circumstances we found, that exception is raised inside the event. For this case potok comes with the built-in mechanism for handling errors.
Let’s see some code:
(defn- on-error
[error]
(js/console.error error))
(def store (ptk/store {:on-error on-error}))
Now, if an exception is raised inside an event it will report it to this function. The return value of on-error callback is ignored.
Five most important rules:
Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Readability counts.
All contributions to potok should keep these important rules in mind.
Unlike Clojure and other Clojure contributed libraries potok does not have many restrictions on contributions. Just open an issue or pull request.
potok is open source and can be found on github.
You can clone the public repository with this command:
git clone https://github.com/funcool/potok
My main motivation is just to simplify a number of concepts that user needs to learn in order to use one-way-flow state management. Reactive streams fit very well this purpose, so I decided not to reinvent the wheel and just use them (in contrast to re-frame or redux as an example).
Potok has very small amount of the code and can be understood and maintained by almost anyone which makes the decision to include it in the production, without the fear of this library becomes unmaintained, easier.
It is just 100 lines of the pretty well-commented code.
Yes, in fact, it is a very useful approach to performing optimistic
updates, because the update event is always the first processed and
the watch and effect events will receive the state already
transformed by the update
function.
Very easy, once you have materialized the state into an atom, you can consume this atom from any react based toolkit (rumext, reagent, etc) in the same way, as you will consume a plain atom with the state.
The unique difference is that if you want to perform a state transformation, you need to define and emit an event for it, instead of direct state atom’s transformation.
Yes, many of them are private, but there is one public: uxbox. It is the pretty big project and it demonstrates that this approach scales very well.
Also, there are some open source projects not connected to the Funcool organization:
potok-rumu - just example project with the simple structure, for showing the potok capabilities. It also uses rum for rendering.
showrum - presentation software, which uses potok for state management
proud - highly opinionated boot template for generating new projects with potok and rum setup
potok is licensed under BSD (2-Clause) license:
Copyright (c) 2015-2019 Andrey Antukh <niwi@niwi.nz> All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Can you improve this documentation? These fine people already did:
Andrey Antukh & Josef PospíšilEdit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close