Liking cljdoc? Tell your friends :D

potok

Introduction

Potok is a tiny (100LOC) reactive streams based state management toolkit for ClojureScript.

Install

Just add this to your dependencies:

[funcool/potok "2.7.0"]

User Guide

Potok lies on top of two concepts: events and reactive store.

Events

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:

Update Event

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)))

Watch Event

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.

Effect Event

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.

Store

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}

Error Handling

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.

Developers Guide

Philosophy

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.

Contributing

Unlike Clojure and other Clojure contributed libraries potok does not have many restrictions on contributions. Just open an issue or pull request.

Source Code

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

FAQ

What is the motivation behind 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.

Can I implement more than one event protocol at the same time?

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.

How can I use potok with React.js based web applications?

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.

Are there some real applications using this pattern?

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

License

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íšil
Edit on GitHub

cljdoc is a website building & hosting documentation for Clojure/Script libraries

× close