Liking cljdoc? Tell your friends :D

re-state

re state License MIT yellow

Re-frame supplimentary library routing dispatched events via statecharts implementing final state machines

TL;DR

Re-state routes re-frame events via statechart interpreter, currently backed by XState library, thus allowing more fine grained event handling. A re-frame component might use a statechart interpreter to dispatch to and handle events related only to the component. The library also implements facilities to isolate component state within re-frame application database, thus making it possible to write real independent standalone components.

Real life example can be found here: https://github.com/MaximGB/TetrisRF

Instalation

{:deps {org.clojure/clojure {:mvn/version "1.10.0"} (1)
        org.clojure/clojurescript {:mvn/version "1.10.520"} (2)
        reagent/reagent {:mvn/version "0.9.0-rc2"} (3)
        re-frame/re-frame {:mvn/version "0.11.0-rc2"} (4)
        maximgb.re-state {:mvn/version "1.2.0"}}} (5)

<1> <2> <3> <4> <5> - Use up-to-date versions for your project here

Usage

There’re three required steps involved in creating a re-frame component which uses library boosted event handling and component isolation:

  • Create a state machine (or statechart) definition which describes your component behaviour in statecharts terms

  • Create an intrpreter (or a service) which will controll the behaviour of a particular component according to state machine definition

  • Send events to your component controlling interpreter using (interpreter-send!) function.

Minimal example

In this example we create a very simple component which displays it’s current state and a button allowing to cycle states. The machine controlling the component behaviour, is very simple, it just cycles through three available states: :one, :two, :three, with no other side effects.

Basic example live demo is here.

(ns maximgb.re-state.example.basic
  (:require [re-frame.core :as rf]
            [reagent.core :as reagent]
            [maximgb.re-state.core :as rs])) (1)


(rs/def-machine basic-machine {:id      :basic-machine
                               :initial :one
                               :states {:one   {:on {:click :two}}
                                        :two   {:on {:click :three}}
                                        :three {:on {:click :one}}}}) (2)


(defn state-cycler [] (3)
  (let [controller (rs/interpreter-start! (rs/interpreter! basic-machine)) (4)
        state-sub (rs/isubscribe-state controller)] (5)
    (fn []
      [:div
       "Current state is: "
       [:div {:style {:display :inline-block
                      :width "5em"}}
        @state-sub]
       [:button
        {:on-click #(rs/interpreter-send! controller :click)} (6)
        "Next state"]])))


(defn -main []
  (reagent/render [:div
                   [:div "State cycler component, press \"Next state\" button to cycle states."]
                   [state-cycler]]
                  (.getElementById js/document "app"))) (7)


(.addEventListener js/window "load" -main)
1Require library core namespace, which contains public API
2Define state machine: initial state, state transition rules
3Define form 2 reagent/re-frame component
4Create and start the controller (or interpreter, or service) interpreting machine defined
5Subscribe to this particular controller state value
6Send :click event to the controller upon button widget click
7Mount the example

Read more on machine difinition in XState documentation

Statecharts DSL

To read more about statecharts please visit https://statecharts.github.io/ or find and read original David Harel "Statecharts: A Visual Formalism for Complex Systems" paper.

Machine definition

A machine is defined with (def-machine machine-name machine-config) macro:

(def-machine my-machine (1)
             {:id :my-machine (2)
              :initial :ready (3)
              :states {:ready {}} (4)
1Machine name, it’s used to define guards, actions and create machine behaviour executing interpreter.
2Machine id, optional, but might help to decypher error messages
3Initial state machine interpreter will start executing the machine behaviour from.
4Machine states definition, here I define only one :ready final state, since it’s the state machine starts from.

States, events, guards and state transition actions

Machine states are defined in machine config under :states key. :states value is a map, where keys are state names and values are state definitions. A finite state machine can be in only one of a finite number of states at any given time. A state definition describes what actions to execute when machine enters the state (:entry key), what actions to execute when machine exits the state (:exit key), and what transitions are possible for the given state (:on key).

A set of transitons for the state is defined under state definition :on key, the key value might be either map or a vector, it describes what events are valid for the state, what are destination states for every event (or to be more precise for every event and guard condition) and what actions to execute upon transition.

State transition actions

When machine transits from one state to another it might execute a set of actions, which being re-frame handlers might affect re-frame application database, request co-effects and issue effects. Actions might be defined in-line in machine config as functions to execute, or they can be designated via action ids. If action is designated in machine config via an id, then action implementation should be defined using one of the following macros:

  • (def-action-db) - similar to re-frame’s (reg-event-db)

  • (def-action-fx) - similar to re-frame’s (reg-event-fx)

  • (def-action-ctx) - similar to re-frame’s (reg-event-ctx)

or their app db isolated counterparts:

  • (def-action-idb)

  • (def-action-ifx)

  • (def-action-ictx)

Action definition example:
(def-action-db
 my-machine (1)
 :my-db-action (2)
 [:my-co-effect-to-inject] (3)
 (fn [db] (4)
   (assoc db :key :value)))
1Machine name the action is defined for
2Machine unique action id
3Optional list of co-effects to inject into re-frame’s co-effects map.
4Action handler

Transition actions a declared using :actions key of transition definition.

The action might be used by machine like this:
(def-machine my-machine
             {:id :my-machine
              :initial :ready
              :states {:ready {:on {:run {:target :running
                                          :actions :my-db-action}}} (1)
                       :running {}}})
1Action is referenced by id, it will be executed when machine transits from :ready to :running state has recieved :run event. Both single action id (or in-line function) and vector with mix of action ids / inline functions are valid.

A simple traffic light example implemented using only states and strict state transition actions live demo is here.

State entry / exit actions

When machine enters to or exits from a state it might execute entry and exit actions. To declare what actions to execute one should use :entry, :exit keys of a state definition.

State entry / exit actions designation
(def-machine my-machine
             {:id :my-machine
              :initial :ready
              :states {:ready {:entry :in-ready (1)
                               :exit  :out-ready (2)
                               :on {:run :running}} (3)
                       :running {}}})
1An action or a vector of actions to execute upon state entry
2An action or a vector of actions to execute upon state exit
3If transition doesn’t involve any actions specific for the transition initiating event then a shortened syntax can be used - just :on {:event :target-state}

The updated traffic light example which uses entry / exit action live demo is here, compare this the previous one.

Action re-frame interceptors declration

Like re-frame event handlers every action might depend on a co-effect(s), similarly like re-frame event handler, every action might be defined with a list of interceptors it needs. An interpreter will collect all the interceptors a transition actions require and inject them into re-frame event handling intercptors chain, thus providing an action with a co-effect it might need.

All action definition macroses allow to provide list of interceptors needed.

Action with interceptors definition
(def-action-fx
  my-machine
  :my-action
  [(inject-cofx :my-cofx)] (1)
  (fn [cofx]
     (let [some-cofx (:my-cofx-value cofx)]
       {:db (assoc-in cofx [:db :my-value]
                           (do-something-with some-cofx)))))
1List of co-effects an action needs

Alongside with well known re-frames (inject-cofx) function, a keyword, symbol, string, number value or a sequence (+ vector) might be used to identify a co-effect an action needs.

  • if a keyword, symbol, string or number value is used then it’s considered to be a co-effect id previously registered with (reg-fx) function call, and it will be automatically wrapped with (inject-cofx).

  • if a sequence or vector is given then its first item is considered to be co-effect id and the rest items will be used as co-effect value and the sequence will be transformed into following (inject-cofx (first s) (rest s)) call.

Guarded transitions

Guarded transitions allow you to transit to differen states depending on some condition. One can analyze event accompanying data and select a state to transit depending on subdomain a data value belongs to, like transit to :too-small state in case event payload value less then 100 and :enough state in case it’s >= 100. The behaviour can be achieved with transition guards.

Event transition destination might be defined using vector whose items are maps with :target and :cond keys, where :cond designates a guard - predicate function used to select transition target state. If the function returns true then a corresponding target is selected.

Guarded transition definition
(def-machine my-machine
             {:id :my-machine
              :initial :ready
              :states {:ready {:on {:run [{:cond   :slow? (1)
                                           :target :run-slowly}

                                          {:cond   :fast? (2)
                                           :target :run-fast}

                                          {:target :run-free}]}} (3)
                       :run-slowly {}
                       :run-fast   {}
                       :run-free   {}}})
1If a guard designated by :slow? id returns true then machine will transit to :run-slowly state.
2If a guard designated by :fast? id returns true then machine will transit to :run-fast state.
3If niether guards will return true then machine will transit to :run-free state.

Both :slow? and :fast? guards implementation should be defined. There’re several macros which allows to define a guard, they are similar to action defining macros:

  • (def-guard-ev)

  • (def-guard-db)

  • (def-guard-fx)

  • (def-guard-ctx)

and their isolated siblings

  • (def-guard-idb)

  • (def-guard-ifx)

  • (def-guard-ictx)

Guards definition
(def-guard-ev (1)
  my-machine
  :slow?
  (fn [event speed] (2)
    (and speed (< speed 7))))

(def-guard-ev
  my-machine
  :fast
  (fn [event speed]
    (and speed (> speed 7)))
1def-guard-ev defines a guard which will recieve only event and it’s payload
2:run event might be accompanied with speed parameter which guard will analyze

A good way to apply transition guards can be found in gauge example. The drag operation starts only when pointer moves about 3 pixes from the starting position, the transition is guarded by the condition guard.

Actions and guards metadata

Both actions and guards can be designated not only as an id, but as a map containing action id under :type keyword and any other key/value pairs which are considered to be action or guard metadata, those key/value information is passed to guard or action as normal Clojure keywordized parameters.

Nested states

Each state node in a machine definition can have a set of nested states under it’s :states key. A state containing nested states is called compound state. If a compound state is not parallel it should have :initial key defined, to point out what sub state a machine should transit to when it transits to the parent compound state. A machine can’t be just in a compound state, one (or several in case of a parallel state) leaf substate is always active. When machine recieves an event, it’s handling goes from leaf states up. If leaf state doesn’t have transition for an event then a transition will be searched in parent state up and so on.

For the time being please see more information at the XState library documentation.

Parallel states

A parallel state node is designated by :type key which should contain :parallel value. Leaf state nodes of a parallel compound state a active simultaneously, as well as they might transition simultaneously if they contain a valid transition for an event being recieved by a machine.

For the time being please see more information at the XState library documentation.

History states

History states allow a statechart to transit to last active compound state child state without explicitly naming it in transition.

For the time being please see more information at the XState library documentation.

Component isolation

The library brings another useful feature which allows to "isolate" component data model and write a component in a way is if it’s the only one and doesn’t share the application database with other components.

To use the feature a code have to be written correspondingly:

  • statechart guards and action should be defined with isolated versions of guards/action definition macros

  • component view should use re-frame subscribtions defined with (reg-isub) function from the library.

The updated gauge example which shows isolation feature in action is here

Isolated actions and guards

Every statechart interpreter is created with an automaticaly generated and application unique path, which, if needed, can be provided by a user explicitly (as the first argument to (interpreter!) function). This path is used to access a section of an re-frame’s application database to store component data into and retrieve component data from.

Both statechart actions and guards can be defined as isolated using following macros:

  • (def-guard-idb)

  • (def-guard-ifx)

  • (def-guard-ictx)

  • (def-action-idb)

  • (def-action-ifx)

  • (def-action-ictx)

The statechart interpreter will access a corresponding application database section and substitute the whole database map in re-frame’s event handling context with just the part of it before executing a guard or action. Thus the handlers will transparently get access to the correct database section still working with it as if they work with entire database.

Isolated subscriptions

To create a subscription to an statechart interpreter isolated application database section one should use (reg-isub) function. The function recieves the same parameters is normal re-frame’s (reg-sub), the only difference is that reactions for such subscription should be created with an interpreter provided as the first parameter

Isolated subscription creation example
(reg-isub
  :my-sub
  (fn [db]
    (:my-key db)))
Isolated reaction creation example
(defn my-comp []
  (let [controller (interpreter-start! (interpreter! my-machine))
    my-sub (subscribe [:my-sub controller])]
    [:div @my-sub]))

Predefined isolated subscribtions

Currently there’re two predefined subscriptions provided by the library:

  • (isubscribe interpreter) - subscribes to the whole interpreter isolated database section

  • (isubscribe-state interpreter) - subscribes to the interpreter active state value

Activities

An activity is an action that occurs over time, and can be started and stopped.

An activity always takes a nonzero amount of time, like beeping, displaying, or executing lengthy computations.
— Harel's original statecharts paper:

Re-state being build on XState uses the same way to define an activity. Activities are specified on the activities property of a state node. When a state node is entered, an interpreter should start its activities, and when it is exited, it should stop its activities.

(rs/def-machine blinking-machine
  {:initial :off
   :states {:off    {:entry       :initialize-db
                     :on         {:toggle    :on}}
            :on     {:on         {:toggle    :off
                                  :blink     {:actions :blink}}
                     :activities [:blinking]}}})

In the above machine configuration, the :blinking activity will start when :on state is entered. Leaving a state will stop the activity.

The activity implementation should be provided either in machine options under :activities section, where each activity should be designated by an unique id. Or uring DSL:

  • (def-acitivty-ev)

  • (def-acitivty-db)

  • (def-acitivty-idb)

  • (def-acitivty-fx)

  • (def-acitivty-ifx)

  • (def-acitivty-ctx)

  • (def-acitivty-ictx)

Activity implementation function should start the activity as it’s side effect and return a function which stops the activity started. The function returned will be called by the re-state interpreter upon state leave to stop the activity.

Activities might request re-frame co-effects to be injected in co-effects map the same way actions do.

Simple activity example can be found here.

Can you improve this documentation?Edit on GitHub

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

× close