Signaali is a ClojureScript/Clojure reactive programming library that implements signals for building reactive systems.
Signaali creates a directed acyclic graph where nodes represent:
create-signalcreate-derivedcreate-effectWhen you modify input data, Signaali:
:stale or :maybe-staleWhen you trigger effect execution:
(require '[signaali.reactive :as sr])
;; Basic signal - can be updated
(def name-signal (sr/create-signal "Alice"))
@name-signal ;; => "Alice"
;; State - only propagates when value actually changes
(def counter (sr/create-state 0))
;; Derived computation - runs when dependencies change
(def greeting (sr/create-derived
(fn []
(str "Hello, " @name-signal "!"))))
;; Memo - like derived but only propagates when result changes
(def expensive-calc (sr/create-memo
(fn []
(expensive-computation @some-input))))
;; Effect - for side effects like DOM updates
(def dom-updater (sr/create-effect
(fn []
(update-dom-element @greeting))))
;; Update signals like atoms
(reset! name-signal "Bob")
(swap! counter inc)
;; This only marks nodes as stale - effects don't run yet
;; Manual effect execution
@dom-updater ; Runs the effect immediately
;; Batch execution (preferred)
(sr/enlist-stale-effectful-node dom-updater)
(sr/re-run-stale-effectful-nodes) ; Runs all enlisted stale effects
Signaali automatically tracks dependencies at runtime by monitoring deref operations:
(def first-name (sr/create-signal "Alice"))
(def last-name (sr/create-signal "Smith"))
;; This derived computation automatically depends on both signals
(def full-name (sr/create-derived
(fn []
(str @first-name " " @last-name)))) ; Signaali tracks both derefs
;; Changing either signal will mark full-name as stale
(reset! first-name "Bob") ; full-name becomes stale
@full-name ; => "Bob Smith" (recomputes lazily)
(def my-effect (sr/create-effect
(fn []
(println "Current value:" @some-signal)
;; Effects can return values
{:status :updated})))
;; First-time enlistment (only needed for effects that haven't run yet)
(sr/enlist-stale-effectful-node my-effect)
(sr/re-run-stale-effectful-nodes)
;; Shortcut: @my-effect does the same as the above two lines
@my-effect
;; After first run, effects auto-enlist when dependencies change
(reset! some-signal "new-value")
(sr/re-run-stale-effectful-nodes) ; my-effect runs automatically
;; Alternative: provide dependencies upfront to avoid first-time enlistment
;; and prevent running the effect before any data changes
(def my-effect-with-deps (sr/create-effect
(fn []
(println "Current value:" @some-signal))
{:deps [some-signal]}))
;; No manual enlistment needed - effect subscribes immediately
(reset! some-signal "another-value")
(sr/re-run-stale-effectful-nodes)
(def timer-effect (sr/create-effect
(fn []
(let [interval-id (js/setInterval #(println "tick") @interval-delay-signal)]
;; Cleanup runs before re-run and when disposed
(sr/on-clean-up #(js/clearInterval interval-id))))))
(def effect1 (sr/create-effect (fn [] (println "First"))))
(def effect2 (sr/create-effect (fn [] (println "Second"))))
;; Ensure effect1 runs before effect2
(sr/run-after effect2 effect1)
(sr/enlist-stale-effectful-node effect1)
(sr/enlist-stale-effectful-node effect2)
(sr/re-run-stale-effectful-nodes)
;; Prints: "First" then "Second"
;; Clean up when no longer needed
(sr/dispose my-effect)
;; This:
;; - Runs cleanup callbacks
;; - Unsubscribes from dependencies
;; - Removes from stale effects set
;; Nodes auto-dispose when they have no watchers (default)
(sr/create-derived
(fn [] @some-signal)
{:dispose-on-zero-signal-watchers true}) ; default
;; Prevent auto-disposal if needed
(sr/create-derived
(fn [] @some-signal)
{:dispose-on-zero-signal-watchers false})
(def mode (sr/create-signal :development))
(def dev-config (sr/create-signal {...}))
(def prod-config (sr/create-signal {...}))
(def active-config (sr/create-derived
(fn []
(case @mode
:development @dev-config ; Only depends on dev-config in dev mode
:production @prod-config)))) ; Only depends on prod-config in prod mode
;; Only propagate when values are not equal (instead of not identical)
(def my-state (sr/create-state initial-value
{:propagation-filter-fn not=}))
;; Custom comparison for complex data
(def complex-state (sr/create-state initial-data
{:propagation-filter-fn (fn [old new]
(not (deep-equal? old new)))}))
;; Make multiple changes
(reset! signal1 "new-value-1")
(reset! signal2 "new-value-2")
(swap! signal3 inc)
;; All effects run together
(sr/re-run-stale-effectful-nodes) ; Single batch update
;; Add logging to see what triggers recomputation
(def debug-derived (sr/create-derived
(fn []
(println "Recomputing with:" @source-signal)
@source-signal)))
(def tracked-effect (sr/create-effect
(fn []
(println "Effect running at:" (js/Date.))
(do-side-effect @some-signal))))
Use appropriate node types: Signals for input, derived for computations, effects for side effects
Batch updates: Use re-run-stale-effectful-nodes instead of manual deref'ing effects
Clean up resources: Always use on-clean-up for resources that need disposal
Keep effects focused: One effect per concern (DOM update, network request, etc.)
Use memos for expensive computations: Especially when the result might not change
Dispose unused nodes: Call sr/dispose or rely on auto-disposal
Order effects when needed: Use run-after for dependent effects
This reactive model provides the foundation for efficient, glitch-free UI updates in web frameworks like Vrac, where DOM changes are triggered by data changes through the signal system.
Can 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 |