ΛↃ LAMBDACOMBINE

Symbolic Systems Infrastructure

09 · Client Reactivity and Loading

DATA-COMPUTED · DATA-SHOW · DATA-ATTR · DATA-INDICATOR
SNAP 10A
SNAP 10A (NASA)

Not all reactivity requires a server round-trip. Four attributes handle common UI patterns using only client-side signal state.

data-computed

A computed signal is derived from an expression over other signals. It is read-only and updates automatically whenever its dependencies change. Declare it with data-computed:name on any element, then reference $name like any other signal.

(:strong :|data-computed:demoupper| "$demotext.toUpperCase()"
          :data-text "$demoupper")

Computed signals are useful for memoizing expressions that appear in multiple bindings, or for deriving a display form without a server call. Note: HTML attribute names are lowercased by the browser before Datastar reads them. Signal names that appear as attribute keys (after the colon) must be lowercase.

data-show

data-show toggles CSS display based on a Datastar expression. When the expression is false the element is hidden via display: none. Set style="display:none" as the initial inline style to prevent a flash of the element before Datastar processes the page.

(:p :data-show "$demotext != ''" :style "display:none"
   "Only visible when input is non-empty")

data-attr

data-attr:name binds any HTML attribute to an expression. When the expression is truthy the attribute is set; when falsy it is removed. The attribute name is written in kebab-case after the colon.

(:button :|data-attr:disabled| "$demotext == ''"
         :|data-attr:aria-disabled| "$demotext == ''"
         "Save")

This covers both functional disabling and ARIA accessibility in one expression. The Datastar docs call out data-attr:aria-* as the standard way to make reactive components accessible.

Live Demo A · client reactivity

Type in the input below. No server is contacted. The computed uppercase preview appears when the input is non-empty; the Save button enables as soon as there is text.

Preview:

data-indicator

data-indicator:name sets signal $name to true while a backend request is in flight, and clears it when the response arrives. Pair it with data-show to display a loading state:

(:button :|data-on:click| (d*:sse-get "/slow-endpoint")
         :|data-indicator:demofetching| ""
         "Fetch")
(:span :data-show "$demofetching" :style "display:none" "Loading...")

The loading text disappears the moment the server sends its first event and Datastar clears $demofetching. This is the right pattern: show progress, wait for the server, let the response update the DOM.

Do not use optimistic updates. Updating the UI before the server confirms the action deceives the user: they see success, only to have it corrected a moment later if the server disagrees. A loading indicator is honest.

Live Demo B · loading indicator

Click Fetch. The server sleeps 1.2 seconds before responding. The Loading indicator is visible during the wait and disappears when the result arrives.

Loading...

Full Source (standalone Hunchentoot)

A self-contained program covering both demos. Load with sbcl --load demo-reactivity.lisp and visit http://localhost:8989.

;;;; -*- Mode: LISP; fill-column: 80; coding: utf-8 -*-

;;;; DEMO-REACTIVITY.LISP --- Client-side reactive attributes and loading
;;;;
;;;; Copyright (C) 2025, 2026 Frederico Muñoz / ΛↃ lambda combine
;;;;
;;;; This file is part of datastar-cl, the Common Lisp SDK for Datastar
;;;;
;;;; License: MIT

;;; Client-side data-* attributes that need no server round-trip:
;;; data-computed, data-show, data-attr. Plus data-indicator with a slow
;;; endpoint to show the loading pattern while a request is in flight.
;;;
;;; Note: HTML attribute names are lowercased by the browser before Datastar
;;; reads them. Signal names that appear as attribute keys (after the colon,
;;; e.g. data-computed:name, data-indicator:name) must be lowercase.
;;;
;;; Run with: sbcl --load demo-reactivity.lisp

(ql:quickload '(:hunchentoot :spinneret :datastar-cl/hunchentoot))

(defpackage #:reactivity
  (:use #:cl #:hunchentoot)
  (:local-nicknames (:sp :spinneret) (:d* :datastar-cl)))
(in-package #:reactivity)

(hunchentoot:define-easy-handler (index :uri "/") ()
  (setf (hunchentoot:content-type*) "text/html")
  (sp:with-html-string
    (:doctype)
    (:html
     (:head (:script :type "module" :src (d*:datastar-url)))
     (:body :data-signals "{name: '', fetching: false}"

            (:h1 "Datastar-CL Reactivity Demo")

            ;; Client-only: no server round-trip for any of these.
            ;;
            ;; data-bind "name" creates signal $name, two-way bound to input.
            ;; data-computed:upper creates read-only derived signal $upper.
            ;; data-show shows/hides the preview paragraph.
            ;; data-attr:disabled / data-attr:aria-disabled are reactive attributes.

            (:h2 "Client reactivity")
            (:input :data-bind "name" :placeholder "Type something")
            (:p :data-show "$name != ''" :style "display:none"
                "Preview: "
                (:strong :|data-computed:upper| "$name.toUpperCase()"
                          :data-text "$upper"))
            (:button :|data-attr:disabled| "$name == ''"
                     :|data-attr:aria-disabled| "$name == ''"
                     "Save")

            (:hr)

            ;; Loading indicator: data-indicator sets $fetching while the
            ;; request is in flight. data-show uses it to toggle "Loading...".
            ;; The /slow endpoint sleeps before responding to make it visible.

            (:h2 "Loading indicator")
            (:button :|data-on:click| (d*:sse-get "/slow")
                     :|data-indicator:fetching| ""
                     "Fetch (slow)")
            (:span :data-show "$fetching" :style "display:none" " Loading...")
            (:span :id "result")))))

(hunchentoot:define-easy-handler (slow :uri "/slow") ()
  (d*:with-sse (gen hunchentoot:*request*)
    (sleep 1.2)
    (multiple-value-bind (s m h) (decode-universal-time (get-universal-time))
      (d*:patch-elements gen
                         (format nil "<span>Response at ~2,'0d:~2,'0d:~2,'0d</span>" h m s)
                         :selector "#result"
                         :mode :inner))))

(hunchentoot:start (make-instance 'hunchentoot:easy-acceptor :port 8989))
(format t "~&Server started on http://localhost:8989~%")

The rest of the Tao

Remaining principles not covered by the previous eight chapters:

(ql:quickload :datastar-cl/brotli) ; adds :br to *default-compression-priority*
;; Default after loading: (:zstd :br) -- prefer zstd, fall back to brotli.
;; To prefer brotli first:
(setf d*:*default-compression-priority* '(:br :zstd))

(defroute guide-reactivity (:get :text/html))