This guide walks you through building a working reactive system with spindel, from setup to signal-driven re-execution.
Add spindel to your deps.edn:
{:deps {org.clojure/clojure {:mvn/version "1.12.0"}
org.replikativ/spindel {:mvn/version "0.1.0"}}}
For ClojureScript, also add:
{:deps {org.clojure/clojurescript {:mvn/version "1.11.132"}
org.replikativ/spindel {:mvn/version "0.1.0"}}}
Spindel provides a convenience namespace that re-exports core APIs:
(require '[org.replikativ.spindel.core :as s :refer [spin signal await track]]
'[org.replikativ.spindel.engine.core :as ec])
Or require individual namespaces for full control:
(require '[org.replikativ.spindel.engine.context :as ctx]
'[org.replikativ.spindel.engine.core :as ec]
'[org.replikativ.spindel.spin.cps :refer [spin]]
'[org.replikativ.spindel.signal :as sig :refer [signal]]
'[org.replikativ.spindel.effects.await :refer [await]]
'[org.replikativ.spindel.effects.track :refer [track]])
Every spindel program starts with an execution context. The context manages state, dependency tracking, and scheduling:
(def context (s/create-execution-context))
All spindel operations require a bound execution context. Bind it using binding:
(binding [ec/*execution-context* context]
;; spindel operations here
)
Signals are mutable reactive values — like atoms that trigger re-execution when changed:
(binding [ec/*execution-context* context]
(def counter (signal 0)))
Signals support the standard atom API:
(binding [ec/*execution-context* context]
@counter ;; => 0
(swap! counter inc)
@counter ;; => 1
(reset! counter 0)
@counter) ;; => 0
Spins are cached reactive computations. Create one with the spin macro:
(binding [ec/*execution-context* context]
(def doubled
(spin
(let [{:keys [new]} (track counter)]
(* 2 new)))))
Key points:
track reads a signal and registers a reactive dependencytrack returns an interval with :new (current value), :old (previous value), and :deltasDeref the spin to get its current value:
(binding [ec/*execution-context* context]
@doubled) ;; => 0
When you update a signal, all spins that track it are marked dirty and re-execute on next deref:
(binding [ec/*execution-context* context]
(swap! counter inc) ;; counter = 1
@doubled ;; => 2 (re-executed: (* 2 1))
(swap! counter inc) ;; counter = 2
@doubled) ;; => 4 (re-executed: (* 2 2))
Re-execution is lazy — spins don't re-execute until their result is needed.
awaitUse await inside a spin to depend on another spin:
(binding [ec/*execution-context* context]
(def tripled
(spin
(let [d (await doubled)]
(* 3 d)))))
;; Dependency chain: counter -> doubled -> tripled
(binding [ec/*execution-context* context]
@tripled ;; => 12 (counter=2, doubled=4, tripled=12)
(reset! counter 10)
@tripled) ;; => 60 (counter=10, doubled=20, tripled=60)
When updating multiple signals, use batch to collect all changes into a single reactive propagation:
(binding [ec/*execution-context* context]
(def x (signal 0))
(def y (signal 0))
(def sum
(spin
(+ (:new (track x))
(:new (track y))))))
;; Without batch: each swap! triggers a separate propagation
;; With batch: one propagation after both updates
(binding [ec/*execution-context* context]
(s/batch
(swap! x inc)
(swap! y inc))
@sum) ;; => 2
When you're done with a context, stop it to clean up background threads:
(s/stop-context! context)
For test code or scripts, use close-context! to also shut down the executor:
(require '[org.replikativ.spindel.engine.context :as ctx])
(ctx/close-context! context)
@ Instead of await Inside Spins;; WRONG — blocks the thread, breaks CPS, no dependency tracking
(spin (let [x @some-spin] ...))
;; CORRECT — CPS-transformed, tracks dependency, non-blocking
(spin (let [x (await some-spin)] ...))
@ (deref) is only for use outside spins (e.g., at the REPL or in non-reactive code). Inside spins, always use await for spins and track for signals.
@ Instead of track for Signals;; WRONG — reads value but doesn't register reactive dependency
(spin (let [x @my-signal] ...))
;; CORRECT — registers dependency, spin re-executes on signal change
(spin (let [{:keys [new]} (track my-signal)] ...))
;; WRONG — throws "No execution context bound"
@my-spin
;; CORRECT
(binding [ec/*execution-context* context]
@my-spin)
(require '[org.replikativ.spindel.core :as s :refer [spin signal await track]]
'[org.replikativ.spindel.engine.core :as ec])
;; Setup
(def ctx (s/create-execution-context))
(binding [ec/*execution-context* ctx]
;; State
(def items (signal []))
(def filter-text (signal ""))
;; Derived computation
(def filtered-items
(spin
(let [all (:new (track items))
query (:new (track filter-text))]
(if (empty? query)
all
(filterv #(clojure.string/includes? (:name %) query) all)))))
(def item-count
(spin
(count (await filtered-items))))
;; Use it
(swap! items conj {:name "Apple"})
(swap! items conj {:name "Banana"})
(swap! items conj {:name "Avocado"})
(println "All items:" @item-count) ;; => 3
(reset! filter-text "A")
(println "Filtered:" @item-count) ;; => 2 (Apple, Avocado)
(reset! filter-text "Av")
(println "Filtered:" @item-count)) ;; => 1 (Avocado)
;; Cleanup
(s/stop-context! ctx)
await, track, and yieldparallel, race, timeout, and moreCan you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |