Liking cljdoc? Tell your friends :D

hyper.core

Public API for the hyper web framework.

Provides:

  • global-cursor, session-cursor, tab-cursor, and path-cursor for state management
  • action macro for handling user interactions
  • navigate function for SPA navigation
  • watch! for observing external state sources
  • create-handler for building ring handlers
Public API for the hyper web framework.

Provides:
- global-cursor, session-cursor, tab-cursor, and path-cursor for state management
- action macro for handling user interactions
- navigate function for SPA navigation
- watch! for observing external state sources
- create-handler for building ring handlers
raw docstring

actioncljmacro

(action & args)

Create a server action expression for use in Datastar event attributes. Returns a Datastar expression string that can be bound to any event.

The action is registered with the current session/tab context and can access state via cursors. Action IDs are deterministic (derived from a per-render counter + tab-id) so that re-renders produce identical HTML, enabling effective brotli streaming compression.

Supports client-side special forms that transmit DOM values to the server:

  • $value — the value of the input/select/textarea that fired the event
  • $checked — the checked state of a checkbox/radio (boolean)
  • $key — the key name for keyboard events (e.g. "Enter", "Escape")
  • $form-data — all named fields in the enclosing form as a map

Example: [:button {:data-on:click (action (swap! (tab-cursor :count) inc))} "Increment"]

;; Capture input value [:input {:data-on:change (action (reset! (tab-cursor :query) $value))}]

;; Keyboard shortcut [:input {:data-on:keydown (action (when (= $key "Enter") (search!)))}]

;; ... with client side check [:input {:data-on:keydown (action {:when "evt.key === 'Enter'"} (search!))}]

;; :when is sugar — an action returns a Datastar expression that composes ;; inside expr, so the guard above is equivalent to: [:input {:data-on:keydown (expr (when (= evt.key "Enter") (action (search!))))}]

;; Named action for testing — :as gives the action a human-readable name ;; that hyper.test/test-page uses as the key in its :actions map [:button {:data-on:click (action {:as "increment"} (swap! (tab-cursor :count) inc))} "Increment"]

;; :key gives the action a stable id (independent of render order) — the ;; same identity story as reactive/async, for actions in keyed lists [:button {:data-on:click (action {:key (:id node)} (delete! (:id node)))} "Delete"]

;; Checkbox [:input {:type "checkbox" :data-on:change (action (reset! (tab-cursor :dark?) $checked))}]

;; Form submission [:form {:data-on:submit__prevent (action (save-user! $form-data))} [:input {:name "email"}] [:button "Save"]]

Applications may define additional client parameters by extending the hyper.client-params/client-param multi-method.

Create a server action expression for use in Datastar event attributes.
Returns a Datastar expression string that can be bound to any event.

The action is registered with the current session/tab context
and can access state via cursors. Action IDs are deterministic
(derived from a per-render counter + tab-id) so that re-renders
produce identical HTML, enabling effective brotli streaming compression.

Supports client-side special forms that transmit DOM values to the server:
- $value     — the value of the input/select/textarea that fired the event
- $checked   — the checked state of a checkbox/radio (boolean)
- $key       — the key name for keyboard events (e.g. "Enter", "Escape")
- $form-data — all named fields in the enclosing form as a map

Example:
  [:button {:data-on:click (action (swap! (tab-cursor :count) inc))}
   "Increment"]

  ;; Capture input value
  [:input {:data-on:change (action (reset! (tab-cursor :query) $value))}]

  ;; Keyboard shortcut
  [:input {:data-on:keydown (action (when (= $key "Enter")
                              (search!)))}]

  ;; ... with client side check
  [:input {:data-on:keydown (action {:when "evt.key === 'Enter'"}
                              (search!))}]

  ;; :when is sugar — an action returns a Datastar expression that composes
  ;; inside `expr`, so the guard above is equivalent to:
  [:input {:data-on:keydown (expr (when (= evt.key "Enter")
                                    (action (search!))))}]

  ;; Named action for testing — :as gives the action a human-readable name
  ;; that hyper.test/test-page uses as the key in its :actions map
  [:button {:data-on:click (action {:as "increment"}
                             (swap! (tab-cursor :count) inc))}
   "Increment"]

  ;; :key gives the action a stable id (independent of render order) — the
  ;; same identity story as reactive/async, for actions in keyed lists
  [:button {:data-on:click (action {:key (:id node)}
                             (delete! (:id node)))}
   "Delete"]

  ;; Checkbox
  [:input {:type "checkbox"
           :data-on:change (action (reset! (tab-cursor :dark?) $checked))}]

  ;; Form submission
  [:form {:data-on:submit__prevent (action (save-user! $form-data))}
   [:input {:name "email"}]
   [:button "Save"]]
   
  Applications may define additional client parameters by extending
  the hyper.client-params/client-param multi-method.
sourceraw docstring

asynccljmacro

(async & args)

Render-time data loading with a placeholder. Spawns the fetch on a background virtual thread, renders a placeholder immediately, and re-renders just this region when the value lands — no manual loading-state plumbing.

Shape (a sibling of reactive):

(h/async [deps] fetch-expr binding & render-body)

  • deps a vector of Watchable sources (like reactive). [] means fetch once per mount; otherwise a change to a dep refetches (stale-while-revalidate — see :reloading below).
  • fetch-expr a single expression evaluated on a background worker thread while the placeholder renders (wrap multiple forms in do). Blocking I/O is fine (it is a virtual thread).
  • binding a destructuring form bound to the status map.
  • render-body hiccup rendered from the status; must return a single rooted element (as with reactive) so the region id can be injected.

The status map is {:status :result :error} where :status is one of:

  • :loading — first load in flight; :result is nil.
  • :ready:result holds the fetched value (a nil result is {:status :ready :result nil}).
  • :error:error holds the throwable; :result keeps the prior value (if any) so you can show stale data with an error.
  • :reloading — a dep changed and a refetch is in flight; :result still holds the previous value (stale-while-revalidate).

Example:

(defn rows-page [req] (let [user-id* (h/path-cursor :user 0)] (h/async [user-id*] (db/fetch-rows @user-id*) {:keys [status result error]} (case status :ready (render-rows result) :error [:p "Failed: " (ex-message error)] :reloading [:div.stale (render-rows result)] [:p "Loading…"]))))

async is a declaration: rendering it registers a region and starts the fetch on a worker thread, so it belongs in the render body like reactive. The region is torn down (and any in-flight fetch interrupted) when it disappears from the view tree, on navigation, or on tab disconnect.

Prefer async for leaf / region-local data that wants its own loading state co-located with its render. For page-level data you want before first paint or shared across regions, load it in a form-2 setup closure into a cursor.

An optional leading opts map accepts :key — a stable identity for the region. Give a :key to any async in a dynamic/keyed list so its in-flight fetch and store follow the item across reorders and sibling shape changes; without one the region id is positional.

Example with a key: (h/async {:key (:id node)} [user-id*] (db/fetch-rows @user-id*) {:keys [status result]} ...)

Render-time data loading with a placeholder.  Spawns the `fetch` on a
background virtual thread, renders a placeholder immediately, and re-renders
*just this region* when the value lands — no manual loading-state plumbing.

Shape (a sibling of `reactive`):

  (h/async [deps] fetch-expr binding & render-body)

- `deps`        a vector of Watchable sources (like `reactive`).  `[]` means
                fetch once per mount; otherwise a change to a dep refetches
                (stale-while-revalidate — see `:reloading` below).
- `fetch-expr`  a single expression evaluated on a background worker thread
                while the placeholder renders (wrap multiple forms in `do`).
                Blocking I/O is fine (it is a virtual thread).
- `binding`     a destructuring form bound to the status map.
- `render-body` hiccup rendered from the status; must return a single rooted
                element (as with `reactive`) so the region id can be injected.

The status map is `{:status :result :error}` where `:status` is one of:
- `:loading`   — first load in flight; `:result` is nil.
- `:ready`     — `:result` holds the fetched value (a nil result is
                 `{:status :ready :result nil}`).
- `:error`     — `:error` holds the throwable; `:result` keeps the prior
                 value (if any) so you can show stale data with an error.
- `:reloading` — a dep changed and a refetch is in flight; `:result` still
                 holds the previous value (stale-while-revalidate).

Example:

  (defn rows-page [req]
    (let [user-id* (h/path-cursor :user 0)]
      (h/async [user-id*]
        (db/fetch-rows @user-id*)
        {:keys [status result error]}
        (case status
          :ready     (render-rows result)
          :error     [:p "Failed: " (ex-message error)]
          :reloading [:div.stale (render-rows result)]
          [:p "Loading…"]))))

`async` is a *declaration*: rendering it registers a region and starts the
fetch on a worker thread, so it belongs in the render body like `reactive`.
The region is torn down (and any in-flight fetch interrupted) when it
disappears from the view tree, on navigation, or on tab disconnect.

Prefer `async` for leaf / region-local data that wants its own loading state
co-located with its render.  For page-level data you want before first paint
or shared across regions, load it in a form-2 setup closure into a cursor.

An optional leading opts map accepts `:key` — a stable identity for the
region.  Give a `:key` to any async in a dynamic/keyed list so its in-flight
fetch and store follow the item across reorders and sibling shape changes;
without one the region id is positional.

Example with a key:
  (h/async {:key (:id node)} [user-id*]
    (db/fetch-rows @user-id*)
    {:keys [status result]}
    ...)
sourceraw docstring

batchcljmacro

(batch & body)

Execute body with all cursor writes batched into a single atomic update.

During the body, cursor reads see the accumulated writes (read-your-writes). After the body completes, all mutations are flushed to app-state* in a single swap!, so the renderer only sees the final state — never an intermediate one.

Side effects (I/O, HTTP calls, DB queries) inside batch work normally — only cursor writes are deferred.

Nested batches are transparent — the inner batch executes within the existing overlay and the outermost boundary handles the flush.

A batch inside background work (future, send-off, fiber) spawned from a render or outer batch ignores the conveyed overlay and creates its own, flushing to the live app-state when its body completes.

Example: ;; Without batch, the renderer might snapshot between cursor updates ;; and show a partial state (e.g. new data with loading still true). (h/action (h/batch (reset! (h/tab-cursor :data) (fetch-data!)) (reset! (h/tab-cursor :loading?) false)))

;; Progress bar: use batch only for the atomic pair, leave the ;; intermediate :loading state unbatched so the renderer picks it up. (h/action (reset! (h/tab-cursor :loading?) true) ;; immediate — shows spinner (let [data (fetch-data!)] (h/batch ;; atomic — one render (reset! (h/tab-cursor :data) data) (reset! (h/tab-cursor :loading?) false))))

Execute body with all cursor writes batched into a single atomic update.

During the body, cursor reads see the accumulated writes (read-your-writes).
After the body completes, all mutations are flushed to app-state* in a
single swap!, so the renderer only sees the final state — never an
intermediate one.

Side effects (I/O, HTTP calls, DB queries) inside batch work normally —
only cursor writes are deferred.

Nested batches are transparent — the inner batch executes within the
existing overlay and the outermost boundary handles the flush.

A batch inside background work (future, send-off, fiber) spawned from a
render or outer batch ignores the conveyed overlay and creates its own,
flushing to the live app-state when its body completes.

Example:
  ;; Without batch, the renderer might snapshot between cursor updates
  ;; and show a partial state (e.g. new data with loading still true).
  (h/action
    (h/batch
      (reset! (h/tab-cursor :data) (fetch-data!))
      (reset! (h/tab-cursor :loading?) false)))

  ;; Progress bar: use batch only for the atomic pair, leave the
  ;; intermediate :loading state unbatched so the renderer picks it up.
  (h/action
    (reset! (h/tab-cursor :loading?) true)     ;; immediate — shows spinner
    (let [data (fetch-data!)]
      (h/batch                                  ;; atomic — one render
        (reset! (h/tab-cursor :data) data)
        (reset! (h/tab-cursor :loading?) false))))
sourceraw docstring

commit!clj

(commit! opt*)

Make the client-reported value of an optimistic official: run its conflict policy and write the result to the backing cursor. Returns the committed value. Action-only.

[:div.handle {:data-on:pointerup (action (commit! w*))}]

Equivalent to (reset! opt* @opt*) plus conflict detection. To validate or transform on the way in, skip the sugar:

(action (reset! w* (clamp @w* 80 640)))

Make the client-reported value of an optimistic official: run its
conflict policy and write the result to the backing cursor.  Returns
the committed value.  Action-only.

  [:div.handle {:data-on:pointerup (action (commit! w*))}]

Equivalent to `(reset! opt* @opt*)` plus conflict detection.  To
validate or transform on the way in, skip the sugar:

  (action (reset! w* (clamp @w* 80 640)))
sourceraw docstring

connected?*clj

Static client-only boolean signal — true while the SSE connection is healthy, false while disconnected/reconnecting. Maintained entirely client-side from Datastar's connection lifecycle (the server cannot report on a connection that is down).

Deref in render/expr yields its Datastar expression; deref in an action throws (connection state is not server-readable).

[:div {:data-show (h/expr (not @h/connected?*))} "Reconnecting…"]

Static client-only boolean signal — true while the SSE connection is
healthy, false while disconnected/reconnecting.  Maintained entirely
client-side from Datastar's connection lifecycle (the server cannot report
on a connection that is down).

Deref in render/`expr` yields its Datastar expression; deref in an action
throws (connection state is not server-readable).

  [:div {:data-show (h/expr (not @h/connected?*))} "Reconnecting…"]
sourceraw docstring

connection*clj

Static client-only signal holding the SSE connection status as a keyword token from connection-states (:connecting, :open, :reconnecting, :error, :closed). Use it for richer connection UX; compare against keyword tokens (they compile to the wire string):

[:span {:data-show (h/expr (= @h/connection* :reconnecting))} "Reconnecting…"] [:span {:data-show (h/expr (= @h/connection* :error))} "Connection lost"]

Static client-only signal holding the SSE connection status as a keyword
token from `connection-states` (`:connecting`, `:open`, `:reconnecting`,
`:error`, `:closed`).  Use it for richer connection UX; compare against
keyword tokens (they compile to the wire string):

  [:span {:data-show (h/expr (= @h/connection* :reconnecting))} "Reconnecting…"]
  [:span {:data-show (h/expr (= @h/connection* :error))}        "Connection lost"]
sourceraw docstring

connection-statesclj

The set of keyword tokens connection* may hold.

The set of keyword tokens `connection*` may hold.
sourceraw docstring

create-handlerclj

(create-handler routes
                &
                {:keys [app-state head static-resources static-dir watches
                        datastar-script base-path middleware render-middleware
                        render-error squint-core-url render-guard max-file-size
                        max-file-count upload-expires-in]
                 :or {app-state (atom (state/init-state))
                      datastar-script server/default-datastar-script}
                 :as opts})

Create a Ring handler for a hyper application.

routes: Vector of reitit routes, or a Var holding routes for live reloading. When a Var is provided, route changes are picked up on the next request without restarting the server — ideal for REPL-driven development.

Options (keyword arguments):

  • :app-state — Atom for application state (default: fresh atom)
  • :datastar-script - Override of the default datastar script tag (as Hiccup) or nil to suppress
  • :squint-core-url — Override of the squint core.js URL used by the client components bundle (default: version-matched jsDelivr CDN). Point at a self-hosted copy for offline/air-gapped deploys.
  • :head — Hiccup nodes appended to the HTML <head>, or (fn [req] ...) -> hiccup
  • :webkit-sse-shim? — Inject a small client shim (into <head>, only for WebKit/Safari user agents) that routes the GET render stream through a native EventSource instead of fetch+ReadableStream, working around a WebKit bug where a large isolated SSE patch is held back until the next write. Other browsers never receive it. Default true; pass false to disable.
  • :base-path — URL path prefix for reverse-proxy deployments where the app is served under a subfolder (e.g. "/my-app"). When set, all internal hyper endpoints (/hyper/events, /hyper/actions, /hyper/navigate) are mounted and referenced under this prefix. Must start with "/" and have no trailing slash.
  • :static-resources — Classpath resource root(s) to serve as static assets
  • :static-dir — Filesystem directory (or directories) to serve as static assets
  • :watches — Vector of Watchable sources added to every page route. Useful for top-level atoms that should trigger a re-render on any page (e.g. a global config or feature-flags atom).
  • :middleware — Vector of Ring middleware fns applied inside the HTTP stack. Each is (fn [handler] (fn [req] ...)). Runs after Hyper's built-in cookie, params, and session middleware, so your middleware sees parsed :cookies, :params, :hyper/session-id, and :hyper/tab-id. Use this for auth, :hyper/env setup, and other request-level concerns. Middleware can also be applied outside create-handler, but will not have access to parsed cookies/params.
  • :render-middleware — Vector of middleware fns applied to every page render. Each is (fn [handler] (fn [req] ...)), identical to Ring middleware. Applied on both initial page loads and SSE re-renders. Per-route :render-middleware wraps inside these.
  • :render-error — Function (fn [error req] -> hiccup) rendered when a view's render-fn throws. May be a Var to pick up redefinitions without restarting the server. Defaults to hyper.render.error/minimal (generic, production- safe). Use hyper.render.error/explain in development to render the message, ex-data, and full stack trace.
  • :not-found — Function (fn [req] -> hiccup) rendered when no route matches, served as a full page with HTTP 404 (and over SSE for client-side navigation). May be a Var to pick up redefinitions. Defaults to hyper.render.error/not-found; pass nil to disable and fall back to reitit's plain-text 404.

The request key :hyper/env is reserved for application-provided context. Ring middleware that sets :hyper/env on the request will have it automatically stashed per-tab and propagated to every SSE re-render and action handler. See env for details.

Example: (def routes [["/" {:name :home :get (fn [req] [:div [:h1 "Home"]])}] ["/about" {:name :about :get (fn [req] [:div [:h1 "About"]])}] ["/api/info" {:name :api-info :hyper/disabled? true :get (fn [req] .. a json api endpoint ..)]])

;; Static routes (def handler (create-handler routes))

;; Live-reloading routes (pass the Var) (def handler (create-handler #'routes))

;; Inject a stylesheet (e.g. Tailwind output) (def handler (create-handler routes :static-resources "public" :head [[:link {:rel "stylesheet" :href "/app.css"}]]))

(def app (start! handler {:port 3000})) ;; Later... (stop! app)

Create a Ring handler for a hyper application.

routes: Vector of reitit routes, or a Var holding routes for live reloading.
        When a Var is provided, route changes are picked up on the next request
        without restarting the server — ideal for REPL-driven development.

Options (keyword arguments):
- :app-state         — Atom for application state (default: fresh atom)
- :datastar-script   - Override of the default datastar script tag (as Hiccup) or nil to suppress
- :squint-core-url   — Override of the squint core.js URL used by the client
                       components bundle (default: version-matched jsDelivr CDN).
                       Point at a self-hosted copy for offline/air-gapped deploys.
- :head              — Hiccup nodes appended to the HTML <head>, or (fn [req] ...) -> hiccup
- :webkit-sse-shim?  — Inject a small client shim (into <head>, only for WebKit/Safari
                       user agents) that routes the GET render stream through a native
                       EventSource instead of fetch+ReadableStream, working around a
                       WebKit bug where a large isolated SSE patch is held back until
                       the next write.  Other browsers never receive it.  Default true;
                       pass false to disable.
- :base-path         — URL path prefix for reverse-proxy deployments where the app is served
                       under a subfolder (e.g. "/my-app"). When set, all internal hyper
                       endpoints (/hyper/events, /hyper/actions, /hyper/navigate) are mounted
                       and referenced under this prefix. Must start with "/" and have no
                       trailing slash.
- :static-resources  — Classpath resource root(s) to serve as static assets
- :static-dir        — Filesystem directory (or directories) to serve as static assets
- :watches           — Vector of Watchable sources added to every page route.
                       Useful for top-level atoms that should trigger a re-render
                       on any page (e.g. a global config or feature-flags atom).
- :middleware        — Vector of Ring middleware fns applied inside the HTTP stack.
                       Each is (fn [handler] (fn [req] ...)).  Runs after Hyper's
                       built-in cookie, params, and session middleware, so your
                       middleware sees parsed :cookies, :params, :hyper/session-id,
                       and :hyper/tab-id.  Use this for auth, :hyper/env setup, and
                       other request-level concerns.  Middleware can also be applied
                       outside create-handler, but will not have access to parsed
                       cookies/params.
- :render-middleware — Vector of middleware fns applied to every page render.
                       Each is (fn [handler] (fn [req] ...)), identical to Ring
                       middleware.  Applied on both initial page loads and SSE
                       re-renders.  Per-route :render-middleware wraps inside these.
- :render-error      — Function `(fn [error req] -> hiccup)` rendered when a
                       view's render-fn throws.  May be a Var to pick up
                       redefinitions without restarting the server.  Defaults
                       to `hyper.render.error/minimal` (generic, production-
                       safe).  Use `hyper.render.error/explain` in development
                       to render the message, ex-data, and full stack trace.
- :not-found         — Function `(fn [req] -> hiccup)` rendered when no route
                       matches, served as a full page with HTTP 404 (and over
                       SSE for client-side navigation).  May be a Var to pick
                       up redefinitions.  Defaults to
                       `hyper.render.error/not-found`; pass `nil` to disable
                       and fall back to reitit's plain-text 404.

The request key :hyper/env is reserved for application-provided context.
Ring middleware that sets :hyper/env on the request will have it automatically
stashed per-tab and propagated to every SSE re-render and action handler.
See `env` for details.

Example:
  (def routes
    [["/" {:name :home
            :get (fn [req] [:div [:h1 "Home"]])}]
     ["/about" {:name :about
                 :get (fn [req] [:div [:h1 "About"]])}]
     ["/api/info" {:name :api-info
                    :hyper/disabled? true
                    :get (fn [req] .. a json api endpoint ..)]])

  ;; Static routes
  (def handler (create-handler routes))

  ;; Live-reloading routes (pass the Var)
  (def handler (create-handler #'routes))

  ;; Inject a stylesheet (e.g. Tailwind output)
  (def handler
    (create-handler routes
                    :static-resources "public"
                    :head [[:link {:rel "stylesheet" :href "/app.css"}]]))

  (def app (start! handler {:port 3000}))
  ;; Later...
  (stop! app)
sourceraw docstring

defccljmacro

(defc & args)

Define a client-side web component, authored in a CLJS dialect (Squint) and compiled to JavaScript on the JVM at macro-expansion time.

(defc temp-gauge [{:keys [value max label]}] (event ::selected [_e] (emit "gauge-selected" {:value value :label label})) (render [:div {:on {:click ::selected}} label ": " value]))

Also defines a server-side render function of the same name, so pages call components like ordinary hiccup functions. Canonical documentation: hyper.component/defc.

Define a client-side web component, authored in a CLJS dialect (Squint)
and compiled to JavaScript on the JVM at macro-expansion time.

  (defc temp-gauge
    [{:keys [value max label]}]
    (event ::selected [_e]
      (emit "gauge-selected" {:value value :label label}))
    (render
      [:div {:on {:click ::selected}} label ": " value]))

Also defines a server-side render function of the same name, so pages
call components like ordinary hiccup functions.  Canonical
documentation: hyper.component/defc.
sourceraw docstring

envclj

(env)
(env key)
(env key default)

Get the request environment, or a specific key from it.

Ring middleware can set :hyper/env on the request to provide context that persists across SSE re-renders and action handlers. Hyper automatically stashes the env per-tab on each HTTP request (page load, action POST, navigation) and propagates it to every subsequent render and action execution.

Use Ring middleware for I/O and request-dependent context (database connections, authenticated user, feature flags read from headers/cookies). Use render middleware to guard renders based on env (e.g. permission checks).

Example: ;; Ring middleware sets :hyper/env (defn wrap-app-env [handler db] (fn [req] (handler (assoc req :hyper/env {:db db}))))

;; Read in a render function (defn my-page [req] (let [db (h/env :db)] [:div "Connected to: " (str db)]))

;; Read in an action [:button {:data-on:click (h/action (let [db (h/env :db)] (db/insert! db ...)))} "Save"]

Get the request environment, or a specific key from it.

Ring middleware can set `:hyper/env` on the request to provide context
that persists across SSE re-renders and action handlers.  Hyper
automatically stashes the env per-tab on each HTTP request (page load,
action POST, navigation) and propagates it to every subsequent render
and action execution.

Use Ring middleware for I/O and request-dependent context (database
connections, authenticated user, feature flags read from headers/cookies).
Use render middleware to guard renders based on env (e.g. permission checks).

Example:
  ;; Ring middleware sets :hyper/env
  (defn wrap-app-env [handler db]
    (fn [req]
      (handler (assoc req :hyper/env {:db db}))))

  ;; Read in a render function
  (defn my-page [req]
    (let [db (h/env :db)]
      [:div "Connected to: " (str db)]))

  ;; Read in an action
  [:button {:data-on:click (h/action
                             (let [db (h/env :db)]
                               (db/insert! db ...)))}
   "Save"]
sourceraw docstring

exprcljmacro

(expr & forms)

Compile Clojure forms into a Datastar expression string for use in data-* attributes, action :when guards, etc.

Signals use atom vocabulary — the same (reset! sig v) that means a server round-trip inside action compiles to an instant client-side assignment here:

(let [open?* (local-signal :open false)] [:button {:data-on:click (expr (swap! open?* not))} "Toggle"] [:div {:data-show (expr @open?*)} "..."])

[:input {:data-on:keydown (expr (when (= evt.key "Enter") (@post "/search")))}]

A server action composes as a client-side value — it registers at render time and contributes its raw @post(...), so it can be gated by client-side control flow (subsuming the action :when guard):

[:input {:data-on:keydown (expr (when (= evt.key "Enter") (action (search! @query*))))}]

Locals splice automatically; evt/el/$signals/JS interop pass through to the client. Canonical documentation: hyper.expr/->expr.

Compile Clojure forms into a Datastar expression string for use in
data-* attributes, action :when guards, etc.

Signals use atom vocabulary — the same (reset! sig v) that means a
server round-trip inside `action` compiles to an instant client-side
assignment here:

  (let [open?* (local-signal :open false)]
    [:button {:data-on:click (expr (swap! open?* not))} "Toggle"]
    [:div {:data-show (expr @open?*)} "..."])

  [:input {:data-on:keydown
           (expr (when (= evt.key "Enter") (@post "/search")))}]

A server `action` composes as a client-side value — it registers at render
time and contributes its raw @post(...), so it can be gated by client-side
control flow (subsuming the action `:when` guard):

  [:input {:data-on:keydown
           (expr (when (= evt.key "Enter") (action (search! @query*))))}]

Locals splice automatically; evt/el/$signals/JS interop pass through
to the client.  Canonical documentation: hyper.expr/->expr.
sourceraw docstring

global-cursorclj

(global-cursor path)
(global-cursor path default-value)

Create a cursor to global state at the given path. Global state is shared across all sessions and tabs — a change to global state triggers a re-render for every connected tab.

Path can be a keyword or vector. If default-value is provided and the path is nil, initializes with default-value.

Example: (global-cursor :theme) (global-cursor [:config :feature-flags]) (global-cursor :user-count 0)

Create a cursor to global state at the given path.
Global state is shared across all sessions and tabs — a change to global
state triggers a re-render for every connected tab.

Path can be a keyword or vector.
If default-value is provided and the path is nil, initializes with default-value.

Example:
  (global-cursor :theme)
  (global-cursor [:config :feature-flags])
  (global-cursor :user-count 0)
sourceraw docstring

local-signalclj

(local-signal path)
(local-signal path default-value)

Create a local Datastar signal (underscore-prefixed). Local signals are client-only: Datastar does not send them to the server, so reset! and swap! are not supported and deref in an action throws.

During render, @local* returns the Datastar expression string (e.g. "$_open"), suitable for data-show, data-text, etc. The signal itself (without deref) can be used as a data-bind value.

Path can be a keyword or a vector of keywords: (local-signal :open? false) ;; → $_open (local-signal :show-menu false) ;; → $_showMenu

Example: (let [open?* (local-signal :open false)] [:div [:button {:data-on:click (str @open?* " = !" @open?*)} "Toggle"] [:div {:data-show @open?*} "Content"]])

Create a local Datastar signal (underscore-prefixed).  Local signals
are client-only: Datastar does not send them to the server, so
`reset!` and `swap!` are not supported and `deref` in an action throws.

During render, `@local*` returns the Datastar expression string
(e.g. `"$_open"`), suitable for `data-show`, `data-text`, etc.
The signal itself (without deref) can be used as a `data-bind` value.

Path can be a keyword or a vector of keywords:
  (local-signal :open? false)   ;; → $_open
  (local-signal :show-menu false)  ;; → $_showMenu

Example:
  (let [open?* (local-signal :open false)]
    [:div
     [:button {:data-on:click (str @open?* " = !" @open?*)} "Toggle"]
     [:div {:data-show @open?*} "Content"]])
sourceraw docstring

(navigate route-name)
(navigate route-name params)
(navigate route-name params query-params)

Create a navigation link using reitit named routes. Returns a map with :href for standard links and :data-on:click__prevent for SPA navigation.

On click, registers an action that:

  1. Looks up the target route's handler
  2. Updates the tab's render fn and route state
  3. Triggers a re-render via SSE
  4. Pushes the new URL via pushState (with title in history state)

The :href ensures right-click → open in new tab works. The title from the target route's :title metadata is resolved eagerly and included in the pushState call so browser history entries have meaningful titles.

route-name: Keyword name of the route params: Optional map of path parameters query-params: Optional map of query parameters

Example: [:a (navigate :home) "Go Home"] [:a (navigate :user-profile {:id "123"}) "View User"] [:a (navigate :search {} {:q "clojure"}) "Search"]

Create a navigation link using reitit named routes.
Returns a map with :href for standard links and :data-on:click__prevent for SPA navigation.

On click, registers an action that:
1. Looks up the target route's handler
2. Updates the tab's render fn and route state
3. Triggers a re-render via SSE
4. Pushes the new URL via pushState (with title in history state)

The :href ensures right-click → open in new tab works.
The title from the target route's :title metadata is resolved eagerly and
included in the pushState call so browser history entries have meaningful titles.

route-name: Keyword name of the route
params: Optional map of path parameters
query-params: Optional map of query parameters

Example:
  [:a (navigate :home) "Go Home"]
  [:a (navigate :user-profile {:id "123"}) "View User"]
  [:a (navigate :search {} {:q "clojure"}) "Search"]
sourceraw docstring

optimisticclj

(optimistic cursor)
(optimistic cursor opts)

Pair a scoped cursor with a client-side signal. The client is authoritative while the user interacts; the cursor is authoritative at rest. Use for continuous gestures (resize, drag, sliders) whose result should persist and propagate like any cursor.

The signal name derives from the cursor's scope and path: (optimistic (session-cursor [:cols 0 :width] 240)) ;; → $sessionCols0Width

During render, @opt* returns the Datastar expression string; during an action it returns the live client-reported value. reset!/swap! in an action write the cursor — the authoritative value — and the client is patched on the next render. commit! makes the client's value official.

opts:

  • :auto-commit? — commit the client-reported value on every action POST
  • :on-conflict — :client-wins (default) | :server-wins | (fn [{:keys [base committed reported]}] → value to commit); :server-wins and fn policies track the base value the client's edit was based on via a companion signal.
Pair a scoped cursor with a client-side signal.  The client is
authoritative while the user interacts; the cursor is authoritative at
rest.  Use for continuous gestures (resize, drag, sliders) whose result
should persist and propagate like any cursor.

The signal name derives from the cursor's scope and path:
  (optimistic (session-cursor [:cols 0 :width] 240))  ;; → $sessionCols0Width

During render, `@opt*` returns the Datastar expression string; during an
action it returns the live client-reported value.  `reset!`/`swap!` in an
action write the cursor — the authoritative value — and the client is
patched on the next render.  `commit!` makes the client's value official.

opts:
- :auto-commit? — commit the client-reported value on every action POST
- :on-conflict  — :client-wins (default) | :server-wins |
                  (fn [{:keys [base committed reported]}] → value to commit);
                  :server-wins and fn policies track the base value the
                  client's edit was based on via a companion signal.
sourceraw docstring

path-cursorclj

(path-cursor path)
(path-cursor path default-value)

Create a cursor backed by URL query parameters. Reading returns the current value of the query param from the tab's route state. Writing updates the query param, which triggers a re-render and a replaceState to update the browser URL bar.

Path can be a keyword or vector of keywords for the query param key(s). If default-value is provided and the query param is nil, initializes with default-value.

Example: (path-cursor :count 0) ;; URL: /?count=0 (path-cursor :search "") ;; URL: /?search=hello

Create a cursor backed by URL query parameters.
Reading returns the current value of the query param from the tab's route state.
Writing updates the query param, which triggers a re-render and a replaceState
to update the browser URL bar.

Path can be a keyword or vector of keywords for the query param key(s).
If default-value is provided and the query param is nil, initializes with default-value.

Example:
  (path-cursor :count 0)     ;; URL: /?count=0
  (path-cursor :search "")   ;; URL: /?search=hello
sourceraw docstring

reactivecljmacro

(reactive & args)

Create a reactive component that re-renders independently when its deps change.

deps is a vector of Watchable sources (atoms, cursors, or any type extending hyper.protocols/Watchable). The body is a hiccup expression that will be wrapped in a div with a stable ID.

When any dep changes, only this component re-renders and a targeted Datastar fragment is sent — the rest of the page is untouched. During full page re-renders, the body is always re-executed (since it may close over parent data not tracked in deps) and the result is cached for future partial renders.

Supports nesting — inner reactive blocks re-execute independently during partial renders triggered by their own deps.

An optional leading opts map accepts :key — a stable identity for the region. Give a :key (or a stable :id on the body's root element) to any reactive in a dynamic/keyed list so its identity follows the item across reorders and sibling shape changes; without one the region id is positional.

Usage: (let [clock* (h/global-cursor :clock)] (reactive [clock*] [:p "The time is: " @clock*]))

;; Multiple deps (let [x* (h/tab-cursor :x 0) y* (h/tab-cursor :y 0)] (reactive [x* y*] [:p "Position: " @x* ", " @y*]))

;; Keyed region in a list — identity follows the node (reactive {:key (:id node)} [grid*] [:div.grid (render-grid @grid*)])

Create a reactive component that re-renders independently when its deps change.

deps is a vector of Watchable sources (atoms, cursors, or any type extending
hyper.protocols/Watchable).  The body is a hiccup expression that will be
wrapped in a div with a stable ID.

When any dep changes, only this component re-renders and a targeted Datastar
fragment is sent — the rest of the page is untouched.  During full page
re-renders, the body is always re-executed (since it may close over parent
data not tracked in deps) and the result is cached for future partial renders.

Supports nesting — inner reactive blocks re-execute independently during
partial renders triggered by their own deps.

An optional leading opts map accepts `:key` — a stable identity for the
region.  Give a `:key` (or a stable `:id` on the body's root element) to
any reactive in a dynamic/keyed list so its identity follows the item
across reorders and sibling shape changes; without one the region id is
positional.

Usage:
  (let [clock* (h/global-cursor :clock)]
    (reactive [clock*]
      [:p "The time is: " @clock*]))

  ;; Multiple deps
  (let [x* (h/tab-cursor :x 0)
        y* (h/tab-cursor :y 0)]
    (reactive [x* y*]
      [:p "Position: " @x* ", " @y*]))

  ;; Keyed region in a list — identity follows the node
  (reactive {:key (:id node)} [grid*]
    [:div.grid (render-grid @grid*)])
sourceraw docstring

reconnectclj

(reconnect)

Return a Datastar expression that re-opens this tab's SSE connection, for binding to an event attribute — e.g. a "Retry" button shown when connection* is :error:

[:button {:data-on:click (h/reconnect)} "Retry"]

This is a soft reconnect: it re-attaches to the still-living tab (within the disconnect grace window), so cursor state, signals, and workers are preserved — unlike a full page reload, which starts a fresh tab. Useful for the connection states Datastar does not auto-retry (:error / :closed).

Reuses the exact same @get the page booted with (endpoint, base-path, and openWhenHidden), so the two can't drift. Must be called in render context.

Return a Datastar expression that re-opens this tab's SSE connection, for
binding to an event attribute — e.g. a "Retry" button shown when
`connection*` is `:error`:

  [:button {:data-on:click (h/reconnect)} "Retry"]

This is a *soft* reconnect: it re-attaches to the still-living tab (within
the disconnect grace window), so cursor state, signals, and workers are
preserved — unlike a full page reload, which starts a fresh tab.  Useful for
the connection states Datastar does not auto-retry (`:error` / `:closed`).

Reuses the exact same `@get` the page booted with (endpoint, base-path, and
`openWhenHidden`), so the two can't drift.  Must be called in render
context.
sourceraw docstring

session-cursorclj

(session-cursor path)
(session-cursor path default-value)

Create a cursor to session state at the given path. Path can be a keyword or vector. If default-value is provided and the path is nil, initializes with default-value.

Example: (session-cursor :user) (session-cursor [:user :name]) (session-cursor :counter 0)

Create a cursor to session state at the given path.
Path can be a keyword or vector.
If default-value is provided and the path is nil, initializes with default-value.

Example:
  (session-cursor :user)
  (session-cursor [:user :name])
  (session-cursor :counter 0)
sourceraw docstring

signalclj

(signal path)
(signal path default-value)

Create a Datastar signal — a reactive client-side variable that syncs between the browser and server.

During render, @signal* returns the Datastar expression string (e.g. "$userName"), suitable for use in data-text, data-show, etc. During action execution, @signal* returns the live value sent by Datastar in the @post() request body.

reset! and swap! update the signal value on the server, which triggers a datastar-patch-signals SSE event to push the new value to the client.

Path can be a keyword or a vector of keywords: (signal :name) ;; → $name (signal :user-name "default") ;; → $userName (signal [:user :name] "") ;; → $user.name

Example: (let [name* (signal :name "")] [:div [:input (bind name*)] [:p {:data-text @name*} ""]])

Create a Datastar signal — a reactive client-side variable that syncs
between the browser and server.

During render, `@signal*` returns the Datastar expression string (e.g.
`"$userName"`), suitable for use in `data-text`, `data-show`, etc.
During action execution, `@signal*` returns the live value sent by
Datastar in the `@post()` request body.

`reset!` and `swap!` update the signal value on the server, which
triggers a `datastar-patch-signals` SSE event to push the new value
to the client.

Path can be a keyword or a vector of keywords:
  (signal :name)                ;; → $name
  (signal :user-name "default") ;; → $userName
  (signal [:user :name] "")     ;; → $user.name

Example:
  (let [name* (signal :name "")]
    [:div
     [:input (bind name*)]
     [:p {:data-text @name*} ""]])
sourceraw docstring

spawn!clj

(spawn! worker-fn)

Spawn a background virtual-thread worker bound to the current view's lifecycle. worker-fn is a zero-arg fn run once on a fresh virtual thread; the framework owns the thread handle and interrupts it on unmount (navigation away, route-handler redefinition, or tab disconnect). Returns nil; a worker is fire-and-forget and communicates by writing state (cursors), which drives the usual declarative re-render.

The worker runs on its own thread with *request* rebound, so cursor writes land and the same tab-cursor/session-cursor/global-cursor/env calls work inside it as in a handler.

Mount-scoped and keyed by call order (like reactive), so a form-1 body that calls spawn! on every render still spawns exactly one worker. Prefer calling it from a form-2 setup closure (runs once per mount), where its intent — "start this worker when the view mounts" — is clearest:

(defn ticker-page [req] (let [now* (h/tab-cursor :now)] (h/spawn! ;; setup — runs once per mount (fn [] (loop [] (reset! now* (System/currentTimeMillis)) (Thread/sleep 1000) (recur)))) ;; interrupted on unmount (fn [req] [:p "Now: " @now*]))) ;; render — pure

For background work, prefer modeling results as state the worker writes; reach for spawn! only when you genuinely need a long-lived loop or blocking consumer tied to the view. For one-shot data loading with a placeholder, prefer async.

Spawn a background virtual-thread worker bound to the current view's
lifecycle.  `worker-fn` is a zero-arg fn run once on a fresh virtual
thread; the framework owns the thread handle and **interrupts it on
unmount** (navigation away, route-handler redefinition, or tab
disconnect).  Returns nil; a worker is fire-and-forget and communicates by
writing state (cursors), which drives the usual declarative re-render.

The worker runs on its own thread with `*request*` rebound, so cursor writes
land and the same `tab-cursor`/`session-cursor`/`global-cursor`/`env` calls
work inside it as in a handler.

Mount-scoped and keyed by call order (like `reactive`), so a form-1 body
that calls `spawn!` on every render still spawns exactly one worker.
Prefer calling it from a **form-2 setup closure** (runs once per mount),
where its intent — "start this worker when the view mounts" — is clearest:

  (defn ticker-page [req]
    (let [now* (h/tab-cursor :now)]
      (h/spawn!                           ;; setup — runs once per mount
        (fn []
          (loop []
            (reset! now* (System/currentTimeMillis))
            (Thread/sleep 1000)
            (recur))))                    ;; interrupted on unmount
      (fn [req] [:p "Now: " @now*])))     ;; render — pure

For background work, prefer modeling results as state the worker writes;
reach for `spawn!` only when you genuinely need a long-lived loop or
blocking consumer tied to the view.  For one-shot data loading with a
placeholder, prefer `async`.
sourceraw docstring

start!clj

(start! handler {:keys [port] :or {port 3000}})

Start the hyper application server.

handler: Ring handler created with create-handler options:

  • :port - Port to run server on (default: 3000)

Returns a stop function. Call (stop! app) to shut down the server and clean up all tab resources (watchers, SSE channels, actions).

Example: (def handler (create-handler routes)) (def app (start! handler {:port 3000})) ;; Later... (stop! app)

Start the hyper application server.

handler: Ring handler created with create-handler
options:
- :port - Port to run server on (default: 3000)

Returns a stop function. Call (stop! app) to shut down the server
and clean up all tab resources (watchers, SSE channels, actions).

Example:
  (def handler (create-handler routes))
  (def app (start! handler {:port 3000}))
  ;; Later...
  (stop! app)
sourceraw docstring

stop!clj

(stop! app)

Stop the hyper application server and clean up all resources.

app: Stop function returned from start!

Stop the hyper application server and clean up all resources.

app: Stop function returned from start!
sourceraw docstring

tab-cursorclj

(tab-cursor path)
(tab-cursor path default-value)

Create a cursor to tab state at the given path. Path can be a keyword or vector. If default-value is provided and the path is nil, initializes with default-value.

Example: (tab-cursor :count) (tab-cursor [:todos :list]) (tab-cursor :count 0)

Create a cursor to tab state at the given path.
Path can be a keyword or vector.
If default-value is provided and the path is nil, initializes with default-value.

Example:
  (tab-cursor :count)
  (tab-cursor [:todos :list])
  (tab-cursor :count 0)
sourceraw docstring

viewclj

(view spec)

Declare a form-3 view that owns an external resource needing explicit teardown (a connection, a file handle, a subscription that is not a Watchable, etc.).

A page handler returns a view instead of hiccup when it must allocate something at mount and release it at unmount. The framework threads the resource immutably through the lifecycle — there is no mutable per-view slot, because the server always re-renders declaratively.

Spec keys:

  • :render (required) (fn [resource req] -> hiccup). resource is the value returned by :mount (nil when there is no :mount). Called on every render; must be pure.
  • :mount (optional) (fn [] -> resource). Runs once when the view mounts; its return value is the resource.
  • :unmount (optional) (fn [resource] -> any). Runs once when the view unmounts (navigation away, handler redefinition, or tab disconnect).

The view (re)mounts when the page first renders or the route handler identity changes; a superseded view is unmounted first.

Prefer the simpler rungs of the ladder when you can: a pure (fn [req] -> hiccup) (form-1) when the view owns nothing, or a setup closure (fn [req] (h/watch! …) (fn [req] hiccup)) (form-2) when it owns only framework-managed subscriptions. Reach for view only for a genuine external resource — frequent need for it is a sign a resource should be modeled as Watchable or owned by the system layer (:hyper/env) instead.

Example: (defn report-page [req] (h/view {:mount (fn [] (db/open-cursor (h/env :db) :reports)) :render (fn [cursor req] [:ul (for [r (db/take! cursor 50)] [:li (:title r)])]) :unmount (fn [cursor] (db/close-cursor cursor))}))

Declare a form-3 view that owns an external resource needing explicit
teardown (a connection, a file handle, a subscription that is not a
Watchable, etc.).

A page handler returns a `view` instead of hiccup when it must allocate
something at mount and release it at unmount.  The framework threads the
resource immutably through the lifecycle — there is no mutable per-view
slot, because the server always re-renders declaratively.

Spec keys:
- :render  (required) `(fn [resource req] -> hiccup)`.  `resource` is the
           value returned by `:mount` (nil when there is no `:mount`).
           Called on every render; must be pure.
- :mount   (optional) `(fn [] -> resource)`.  Runs once when the view
           mounts; its return value is the resource.
- :unmount (optional) `(fn [resource] -> any)`.  Runs once when the view
           unmounts (navigation away, handler redefinition, or tab
           disconnect).

The view (re)mounts when the page first renders or the route handler
identity changes; a superseded view is unmounted first.

Prefer the simpler rungs of the ladder when you can: a pure `(fn [req] ->
hiccup)` (form-1) when the view owns nothing, or a setup closure
`(fn [req] (h/watch! …) (fn [req] hiccup))` (form-2) when it owns only
framework-managed subscriptions.  Reach for `view` only for a genuine
external resource — frequent need for it is a sign a resource should be
modeled as Watchable or owned by the system layer (:hyper/env) instead.

Example:
  (defn report-page [req]
    (h/view
      {:mount   (fn []          (db/open-cursor (h/env :db) :reports))
       :render  (fn [cursor req] [:ul (for [r (db/take! cursor 50)]
                                        [:li (:title r)])])
       :unmount (fn [cursor]     (db/close-cursor cursor))}))
sourceraw docstring

watch!clj

(watch! source)

Watch an external source for changes, triggering a re-render of the current tab when it changes. Source must satisfy the hyper.protocols/Watchable protocol (extended by default for atoms, refs, vars, and any IRef).

A watch is a mount-scoped subscription, keyed by source identity for dedup. Prefer a form-2 setup closure (runs once per mount):

(defn my-page [req] (let [results (db/query-atom (get-in req [:hyper/route :path-params :id]))] (watch! results) ;; setup — runs once per mount (fn [req] [:p "Count: " (count @results)])))

It is idempotent, so a form-1 render body also works (re-subscribing each render); that path triggers the render-purity warning nudging you to form-2.

The mount boundary is keyed on [handler path-params], so a source derived from a path-param re-subscribes when that path-param changes: the page remounts, tearing down the old watch and running setup for the new one. Watches are cleaned up on remount and tab disconnect.

Watch an external source for changes, triggering a re-render of the current
tab when it changes. Source must satisfy the hyper.protocols/Watchable protocol
(extended by default for atoms, refs, vars, and any IRef).

A watch is a mount-scoped subscription, keyed by source identity for dedup.
Prefer a **form-2 setup closure** (runs once per mount):

  (defn my-page [req]
    (let [results (db/query-atom (get-in req [:hyper/route :path-params :id]))]
      (watch! results)                     ;; setup — runs once per mount
      (fn [req] [:p "Count: " (count @results)])))

It is idempotent, so a form-1 render body also works (re-subscribing each
render); that path triggers the render-purity warning nudging you to form-2.

The mount boundary is keyed on `[handler path-params]`, so a source derived
from a path-param re-subscribes when that path-param changes: the page
remounts, tearing down the old watch and running setup for the new one.
Watches are cleaned up on remount and tab disconnect.
sourceraw docstring

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close