Skip to content

roman01la/solid-cljs

Repository files navigation

solid-cljs

Clojars Project

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

How it works

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.

Installation

  1. npm i solid-js -D
  2. {:deps {com.github.roman01la/solid-cljs {:mvn/version "0.1.1"}}}

Example

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

API

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

Attributes

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?}}

Rendering

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

Lifecycle

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

Reactive utilities

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 [] ...))

Store utilities

Create data store via createStore

(let [[store set-store] (s/store {:value 0})]
  ...)

Playground

source in dev/app/core.cljs

  1. Install NPM deps
  2. Run local build yarn example
  3. Go to localhost:3000

Testing

Run the test suite:

yarn test

Or watch tests during development:

yarn test:watch

About

ClojureScript bindings to Solid

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 4

  •  
  •  
  •  
  •