clojure-sweden

Sam 2026-03-28T10:31:35.208999Z

Jag håller på att titta på replicant och det dök precis upp state management https://youtu.be/N85uFVL7YF0?list=PLXHCRz0cKua5hB45-T762jXXh3gV-bRbm Så nu har jag plötsligt sånt här i min kodbas:

(defn perform-actions [state event-data]
  (mapcat
   (fn [action]
     (prn (first action) (rest action))
     (or
      (onboarding/perform-action state action)
      (case (first action)
        :action/assoc-in
        [(into [:effect/assoc-in] (rest action))])))
   event-data))
vilket är väldigt fint och funktionellt men har ganska lite med onboarding av nya kunder att göra. @pez hur gör du? @emil0r jag hittade https://github.com/emil0r/convey men det är arkiverat, hur gör du? 🙂

pez 2026-03-28T11:57:57.513999Z

Gomorron! Jag gör olika varje gång. 😃 Men det finns väl ändå något mönster om man kisar. Jag kommer från re-frame-hållet och tycker om hur man separerar state-hantering och andra effekter där. Jag kallar mitt lilla “ramverk” för Uniflow. Citat-tecken för det är mer av ett recept som jag blandar om efter behov och humör. Några begrepp: • Actions (`axs`): Liknar väl det du har där, en vektor som Replicant (och andra deltagare) dispatchar mot min event-handler. ◦ En action (`ax`), får app-state och event-data och argument, lämnar ifrån sig en map med ny app-state, effekter, och dispatches (actions). (Mappen är enbart data, en action kan testas på samma sätt som en ren funktion). • Effects (`exs`): En vektor av: ◦ Effect (`ex`): En beskrivning av en effekt som ska utföras. • Dispactches (`dxs`): En vektor av ax Så en action beskriver typ “givet nuvarande läge och dessa parametrar skall ny app-state se ut så här, och de här effekterna utföras, och sedan ska dessa actions hanteras”. Det här sköter min event-handler om. Här från en app jag jobbat med för ett tag sedan:

(defn get-triggered-when-actions [{:db/keys [when-actions] :as state}]
  (let [triggered-actions (mapcat (fn [[when-key actions]]
                                    (when (state when-key)
                                      actions))
                                  when-actions)
        new-when-actions (vec (remove (fn [[when-key _]]
                                        (state when-key))
                                      when-actions))]
    (if (seq triggered-actions)
      (into [[:db/ax.assoc :db/when-actions new-when-actions]] triggered-actions)
      [])))

(defn event-handler [replicant-data event-actions]
  (let [actions (into (get-triggered-when-actions @db/!state) event-actions)
        axfx-data (assoc replicant-data :ex/event-handler event-handler)
        {:ex/keys [fxs dxs db]} (try
                                  (ax/handle-actions @db/!state axfx-data actions)
                                  (catch :default e
                                    {:ex/fxs [[:log/ax.log :error (ex-info "event-handler/handle-action error"
                                                                           e
                                                                           ::event-handler-handle-actions)]
                                              [:ex/fx.dispatch [[:state/ax.update :ui/toast-messages conj
                                                                 [:i18n :app/i18n.unknown-event-error]]]]]}))]
    (when db
      (reset! db/!state db))
    (when dxs
      (event-handler replicant-data dxs))
    (when fxs
      (try
        (doseq [fx fxs]
          (when fx
            (when-not (= :log/fx.log (first fx))
              (when js/goog.DEBUG
                (js/console.debug "Triggered effect" fx)))
            (try
              (fx/perform-effect! event-handler replicant-data fx)
              (catch :default e
                (logging/log! :error (ex-info "fx/perform-effect! Effect failed"
                                              {:error e
                                               :effect fx}
                                              ::event-handler-perform-effects))
                (throw e)))))
        (catch :default e
          (logging/log! :error (ex-info "fx/perform-effects! error"
                                        e
                                        ::event-handler-perform-effects)))))))
when-actions är en grej jag hittat på för att kunna dispatcha actions när något givet app-state uppfylls. Om du struntar i den och i loggningen så blir “givet nuvarande läge och dessa parametrar skall ny app-state se ut så här, och de här effekterna utföras, och sedan ska dessa actions hanteras” kvar, tycker jag. 😃 1. För varje: (ax/handle-actions @db/!state axfx-data actions) a. (when db (reset! db/!state db)) b. (when dxs (event-handler replicant-data dxs)) c. (when fxs (doseq [fx fxs] (fx/perform-effect! event-handler replicant-data fx))) Notera handle-actions, perform-effects.
(defn handle-actions [state axfx-data actions]
  (reduce (fn [{state :ex/db :as acc} action]
            (let [{:ex/keys [fxs dxs db]} (handle-action state axfx-data action)]
              (when (and js/goog.DEBUG
                         (exists? js/window))
                (debounce/dispatch!
                 {:id (first action)
                  :type :dispatch
                  :thunk #(js/console.log "Triggered action" (first action) action)
                  :timeout 250}))
              (cond-> acc
                db (assoc :ex/db db)
                dxs (assoc :ex/dxs dxs)
                fxs (update :ex/fxs into fxs))))
          {:ex/db state
           :ex/fxs []}
          (remove nil? actions)))
En ren funktion. Jag använder oftast core.match för att kunna läsa mina action-tabeller. Och om det är en större app så har jag en handle-action per domän, och den första handle-action delar ut hanteringen till olika domäner:
(defn handle-action [state axfx-data action]
  (let [enriched-action (->> action
                             (enrich-action-from-state state)
                             (enrich-action-from-replicant-data axfx-data))]
    (case (-> enriched-action first namespace)
      "foo" (foo-ax/handle-action state axfx-data enriched-action)
      "bar" (bar-ax/handle-action state axfx-data enriched-action)
      ,,,

      (match enriched-action
        ,,,

        [:app/ax.global-keydown event]
        (let [{:server/keys [location-relations-lookup]} state
              {:keys [^boolean ctrlKey ^boolean altKey ^boolean metaKey code target]} event
              dev-toggle-shortcut? (and (:user-works-here location-relations-lookup)
                                        ctrlKey
                                        altKey
                                        (= code "KeyD")
                                        (not metaKey))
              ; Don't trigger when typing into inputs/textareas or contenteditable regions
              tag (some-> target .-tagName (string/lower-case))
              in-text-field? (or (= tag "input")
                                 (= tag "textarea"))
              in-contenteditable? (some? (when target
                                           (.closest target "[contenteditable=true]")))]
          (when-not (or in-text-field?
                        in-contenteditable?)
            (cond-> {:ex/fxs [[:dom/fx.stop-propagation]]}
              dev-toggle-shortcut?
              (assoc :ex/db (update state :dev/dev-panel-open? not)))))
        
        ,,,
       
        [:db/ax.assoc & args]
        {:ex/db (apply assoc state args)}

        [:db/ax.assoc-in path v]
        {:ex/db (assoc-in state path v)}

        [:db/ax.dissoc & args]
        {:ex/db (apply dissoc state args)}

        [:db/ax.dissoc-in path k]
        {:ex/db (update-in state path dissoc k)}

        [:db/ax.update k f & args]
        {:ex/db (apply update state k f args)}

        [:db/ax.update-in path f & args]
        {:ex/db (apply update-in state path f args)}
        :else
        (do
          (js/console.error "Unknown action:" enriched-action)
          {})))))
Just denna action är inte superlätt att testa, för jag fuskar ibland. 😃 Domän-hanterare handlar då mer om domänen än annat. Inte det bästa exemplet på det, men ändå:
(defn handle-action [state _replicant-data action]
  (match action
    ,,,

    [:foo/ax.load-thing thing-id]
    (let [endpoint  (-> state :app/config :app/endpoint)
          {:auth/keys [jwt-token]
           :app/keys [auth]} state]
      {:ex/db (-> state
                  (merge {:app/thing-loading? true
                          :app/loading-thing-id thing-id})
                  (dissoc :app/thing-loading-error))
       :ex/fxs [[:ajax/fx.http-xhrio {:method          :get
                                      :jwt-token       jwt-token
                                      :auth            auth
                                      :uri             endpoint
                                      :params          {:thing-id thing-id}
                                      :response-format (ajax/transit-response-format {:reader t/reader})
                                      :on-success      [:foo/ax.load-thing-success]
                                      :on-failure      [:foo/ax.load-thing-failure]}]]})
    
    ,,,
    ))
Forts i nästa kommentar…

pez 2026-03-28T12:00:08.871769Z

Effekter utförs:

(defn perform-effect! [event-handler {:keys [^js replicant/js-event] :as replicant-data} effect]
  (when (and js/goog.DEBUG
             (exists? js/window))
    (debounce/dispatch!
     {:id (first effect)
      :type :dispatch
      :thunk #(js/console.log "Performing effect:" (first effect) effect)
      :timeout 250}))

  (case (-> effect first namespace)
    "log" (log-fx/perform-effect! event-handler replicant-data effect)
    "router" (router-fx/perform-effect! event-handler replicant-data effect)
    ,,,

    (match effect
      [:dom/fx.focus-element element]
      (.focus element)

      [:dom/fx.submit-form element]
      (.requestSubmit (.closest element "form"))

      [:dom/fx.set-custom-validity element message]
      (do
        (.setCustomValidity element "")
        (when (not (some-> element .-validity .-valid))
          (.setCustomValidity element message)))

      [:dom/fx.select element]
      (js/requestAnimationFrame #(.select element))

      [:dom/fx.prevent-default]
      (.preventDefault js-event)

      [:dom/fx.stop-propagation]
      (.stopPropagation js-event)

      [:dom/fx.set-input-text element text]
      (set! (.-value element) text)

      [:dom/fx.add-listener target event-type handler-fn]
      (.addEventListener target event-type handler-fn)

      [:dom/fx.remove-listener target event-type handler-fn]
      (.removeEventListener target event-type handler-fn)

      [:dom/fx.add-window-event-listener event-type actions]
      (.addEventListener js/window event-type #(event-handler {:replicant/js-event %} actions))

      [:dom/fx.add-document-event-listener event-type actions]
      (.addEventListener js/document event-type #(event-handler {:replicant/js-event %} actions))

      [:ex/fx.dispatch actions]
      (event-handler replicant-data actions)

      [:ex/fx.sequenced-dispatch id actions]
      (sequential/dispatch! {:id id
                             :type :dispatch
                             :thunk #(event-handler replicant-data actions)})

      [:ex/fx.raf-dispatch actions]
      (js/requestAnimationFrame #(event-handler replicant-data actions))

      [:ex/fx.dispatch-debounce replicant-data id actions ms]
      (debounce/dispatch! {:id id
                           :type :dispatch
                           :thunk #(event-handler replicant-data actions)
                           :timeout ms})

      [:ex/fx.dispatch-timeout id actions ms]
      (timeout/dispatch! {:id id
                          :type :dispatch
                          :thunk #(event-handler replicant-data actions)
                          :timeout ms})
      
      [:ex/fx.refresh]
      (utils/refresh)

      [:ajax/fx.http-xhrio request]
      (http/http-effect! (assoc request :dispatch! (partial event-handler replicant-data)))

      [:app/fx.post-message-to-parent message]
      (utils/post-message-to-parent (clj->js message))

      [:app/fx.print]
      (js/window.print) 

      [:dom/fx.set-document-title title]
      (set! js/document.title title)

      [:dom/fx.dispatch-custom-event event-id]
      (.dispatchEvent js/window (js/CustomEvent. event-id)))))
Effekter får inte under några omständigheter rota upp data ur app-state och absolut inte skriva i app-state (säger detta mest till Copilot som behöver påminnas ibland). Effekter får event-handler som argument dock, så om de behöver uppdatera app-state kan de dispatcha [[:db/ax.assoc-in [:här :nånstans] 42]]. Ursäkta text-vägg. Jag borde kanske skriva om Uniflow någonstans på webben… Det fungerar väldigt bra för mig. Dels för att jag förstår det 😃 Men också antagligen mycket för att det är bara ett recept och jag kan skräddarsy efter behov. I #epupp har jag behov av promise-medvetna effekter så där gjorde jag så. I Epupp-projektet finns Uniflow beskrivet av och för AI-agenter, för övrigt. Men det är då den specifika implementationen där, för att inte förvirra den stackars roboten.

pez 2026-03-28T12:10:47.445079Z

Något jag inte lärt mig än är mönstret med prepare som separerar app-state från vad vyerna behöver. Jag tror att det skulle ta hela grejen till nästa nivå för mig. Om du inte hittat, så är den här presentationen av @christian767 alldeles fantastiskt bra: https://youtu.be/AGTDfXKGvNI?si=-l46-0oDC7wJgCGb

pez 2026-03-28T12:11:36.352429Z

#54!

Sam 2026-03-28T12:16:20.432099Z

Jag tycker definitivt att du ska skriva om Uniflo! om det är som @emil0r skrev här https://clojurians.slack.com/archives/C0536F336/p1774697922790789 och du tycker att det finns bättre alternativ så... Replicant verkar ju ganska poppis nu

Sam 2026-03-28T12:17:00.164839Z

Jag hade typ tänkt vänta 6 månader tills saker som https://www.reddit.com/r/Clojure/comments/1rfgo75/announcing_hyper_a_reactive_serverrendered_web/ blir mer mogna men nu behövde jag en liten hemsida här så det gick inte.

pez 2026-03-28T12:25:14.655839Z

Verkar vara mer än en hemsida om du behöver framtida tech? 😃

pez 2026-03-28T12:25:40.356479Z

Även Replicant verkar lite i överkant för en hemsida.

Sam 2026-03-28T12:26:58.246879Z

Definitivt! Jag bara vet inget annat... Förutom cljd då. Vilket ser mer och mer attraktivt ut...

pez 2026-03-28T12:27:31.997609Z

Behöver din hemsida en server?

pez 2026-03-28T12:29:08.423929Z

Jag är förtjust i statiska, genererade, hemsidor. Christian och Magnars har gjort en del grejor för det också.

Sam 2026-03-28T12:29:47.980959Z

Den har redan en server! Vi håller på att lansera en shoppinggrej, och återförsäljare ska själva kunna gå in och mappa sin artikelfeed mot vårt interna format. Jag ska skapa någon slags drag-and-drop upplevelse där de skriver in en url till sin feed och den plockar in några artiklar och sen ska användaren kunna drag-and-droppa sina nycklar till våra nycklar.

pez 2026-03-28T12:30:04.625779Z

Och den här, gjord av min kollega där jag jobbar just nu: https://github.com/anteoas/eden/blob/main/docs/getting-started.md

✍️ 1
pez 2026-03-28T12:30:50.654539Z

Jag är också sjukt nyfiken på Datastar.

pez 2026-03-28T12:33:49.098779Z

Jag tror att Uniflow och nexus är ganska lika när det gäller de saker @emil0r tar upp. Jag vill verkligen inte ha komponenter i mina appar. Replicant sitter som en handske för mig.

Sam 2026-03-28T12:33:56.494519Z

Datastar känns som framtiden. Flutter & Firebase fick mig att älska tanken på att bara ha en frontend (ingen backend), så det känns som att det också ska finnas en lösning som bara har en backend.

pez 2026-03-28T12:34:34.973029Z

Datastar är här och nu. Men visst är det minblowing.

emil0r 2026-03-28T11:31:19.848049Z

@sam.hedin Vi använder https://github.com/cjohansen/nexus på jobbet istället för det jag skrev, som var mest för att det inte fanns några alternativ

👀 1
Sam 2026-03-28T11:35:58.332459Z

Tacks, ska testa!

pez 2026-03-28T12:04:29.035219Z

Jag har bara använt nexus i projekt som andra skrivit. Det är nog bra, men jag gillar inte att hålla på att registerera saker (en av sakerna jag inte tagit med mig från re-frame). Jag är inte tillräckligt bra med REPLen för att få det att bli bekvämt. Dessutom är jag väldigt förtjust i att skrädda-sy de här delarna av applikationen.

emil0r 2026-03-28T11:32:30.331309Z

En fördel med att använda något som nexus är att du kan dra nytta av andras erfarenheter med det

emil0r 2026-03-28T11:38:42.790789Z

nexus har några svaga punkter • Det är svårt att gå in i replicant/nexus utifrån replicant/nexus ◦ Du behöver fånga upp en dispatch som du kan sända vidare om du vill ha en möjlighet att gå tillbaka in i nexus utifrån ◦ Det kan vara svårt att kommunicera in data in i nexus/replicant från ren js kod om du skulle behöva det • dataspex som finns för inspektion är inte lika moget som något som re-frame-10x • Iom att du inte har tillgång till någon som helst mutability så blir kommunikation mellan delar i en widget/component något du behöver göra via events ◦ kan räknas som styrka eller svaghet beroende på var du står i den debatten ◦ Gör det svårare att jobba med encapsulation i widgetar/components som är något du kan argumentera starkt för att du ska göra. Implementationsdetaljer i en widget ska inte läcka uppåt • Att lyfta upp data från widgets du själv har lagat löser vi genom att göra custom events där vi sedan kan lägga till vad vi behöver ◦ Övre lager i koden behöver ta hand om dessa events

Sam 2026-03-28T12:08:05.982569Z

> Iom att du inte har tillgång till någon som helst mutability så blir kommunikation mellan delar i en widget/component något du behöver göra via events > • kan räknas som styrka eller svaghet beroende på var du står i den debatten Den här blev jag orolig för när jag såg hans kod. Jag vill kunna se all relevant kod i närheten av där den renderas, inte behöva göra en dispatch över filer. Jag använder re-dash (reframe för clojuredart) i Disorganized och där använder jag effects och dispatch för vissa saker, men enkla småsaker kan jag göra direkt i varje widget och det gör saker mycket snyggare.

emil0r 2026-03-28T17:53:23.197769Z

Det är ett mindre problem i praktiken. Du skapar actions och effects som används enbart internt

emil0r 2026-03-28T17:55:39.521489Z

Vi har som praxis att döpa allt i stilen effects.<widget>/<name>

emil0r 2026-03-28T17:56:15.483489Z

Du definierar allt tillsammans med koden

emil0r 2026-03-28T17:56:45.803139Z

Och importerar sedan allt till ett lokalt ställe

🙏 1