ClojureScript bindings to Solid. solid-cljs wraps Solid API documented at docs.solidjs.com. Compatible with Solid 1.9
Early alpha, unstable and highly experimental
Solid.js achieves high performance through fine-grained reactivity. Unlike React, Solid doesn't re-render entire components — instead, it updates only the specific DOM nodes that depend on changed values. To make this work, Solid's JSX compiler wraps reactive expressions in functions, creating precise reactive boundaries.
Since ClojureScript doesn't use JSX, solid-cljs provides macros that act as a compiler, automatically wrapping expressions to preserve Solid's fine-grained reactivity. This is why we provide Solid-specific versions of common Clojure macros like if, when, or, if-let, if-some, some->, etc.
Why not just use Clojure's built-in macros?
Using standard Clojure macros would break Solid's reactivity model:
;; ❌ Using clojure.core/or - evaluates eagerly, loses reactivity
($ :h1 (or @title "Default"))
;; ✅ Using s/or - preserves reactive boundaries
($ :h1 (s/or @title "Default"))The solid-cljs macros ensure that reactive expressions remain wrapped in functions, so Solid can track dependencies and update only what's necessary. This gives you Solid's performance benefits while writing idiomatic ClojureScript.
npm i solid-js -D{:deps {com.github.roman01la/solid-cljs {:mvn/version "0.1.1"}}}
(ns app.core
(:require [solid.core :as s :refer [$ defui]]))
(defui app []
(let [value (s/signal 0)]
(s/effect
#(println "value:" @value))
($ :div
($ :button {:on-click #(swap! value inc)} "+")
@value
($ :button {:on-click #(swap! value dec)} "-"))))
(s/render ($ app) (js/document.getElementById "root"))Functions and macros below are a part of solid.core namespace:
defui creates Solid component. Supports docstrings and metadata.
(defui button
"A styled button component."
{:private true}
[{:keys [on-click children]}]
($ :button {:on-click on-click} children))$ creates Solid element
($ button {:on-click #(prn :pressed)} "press")The :class attribute supports multiple formats:
;; String
{:class "btn primary"}
;; Keyword
{:class :btn}
;; Vector of classes
{:class [:btn :btn-primary "active"]}
;; Map for conditional classes
{:class {:active @is-active?
:disabled @is-disabled?}}Conditional rendering with if and when, via component
(s/if test ... ...)
(s/when test ...)Conditional rendering with bindings via if-let and when-let:
(s/if-let [user @current-user]
($ :div "Hello, " (:name user))
($ :div "Please log in"))
(s/when-let [user @current-user]
($ :div "Hello, " (:name user)))Nil-checking (not falsiness) with if-some and when-some. Unlike if-let, these render for falsy values like false or 0:
(s/if-some [count @item-count]
($ :span count " items") ;; shows "0 items" when count is 0
($ :span "Loading..."))
(s/when-some [count @item-count]
($ :span count " items"))Fallback values with or:
($ :h1 (s/or @custom-title "Default Title"))Nil-safe threading with some-> and some->>:
;; Returns nil if any step is nil
(s/some-> @user :profile :settings :theme)
;; Thread as last argument
(s/some->> @items (filter :active) first :name)Conditional rendering with cond, via and components
(s/cond
(= x 1) ($ button {})
(= y 2) ($ link {})
:else ($ text {}))List rendering via <For> component
(def xs [1 2 3])
(s/for [[x idx] xs] ;; <- index is added implicitly
($ :li x))List rendering via <Index> component
(def xs [1 2 3])
(s/index [[x idx] xs] ;; <- index is added implicitly
($ :li @x))Catching rendering errors via component
(s/try
($ my-component {})
(catch err
($ error-view {})))Run the code after initial rendering via onMount
(s/on-mount (fn [] (prn :component-mounted)))Clean up side effects via onCleanup
(s/on-cleanup (fn [] (prn :component-unmounted)))Create atom-like signal via createSignal
(let [state (s/signal [])]
...)Access DOM element via ref
(let [ref (s/ref)]
(s/on-mount (fn [] (js/console.log @ref)))
($ :button {:ref ref} "press"))Execute side effects after DOM update via createEffect
(s/effect (fn [] (prn @value)))Execute side effects before DOM update via createRenderEffect
(s/render-effect (fn [] (prn @value)))Batching multiple signals updates via batch
(s/batch
(swap! value1 inc)
(swap! value2 inc))Create reactive computation for side effects, via createComputed
(s/computed
(prn @value))Separate tracking from re-execution, via createReaction
(let [track (s/reaction (prn :update))]
;; run the reaction next time `s` changes
(track (fn [] @s)))Created cached computation via createMemo
(s/memo (fn [] ...))Create data store via createStore
(let [[store set-store] (s/store {:value 0})]
...)source in dev/app/core.cljs
- Install NPM deps
- Run local build
yarn example - Go to localhost:3000
Run the test suite:
yarn testOr watch tests during development:
yarn test:watch