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? 🙂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…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.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
https://bulbapedia.bulbagarden.net/wiki/Psyduck_%28Pok%C3%A9mon%29#/media/File:0054Psyduck.png
#54!
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
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.
Verkar vara mer än en hemsida om du behöver framtida tech? 😃
Även Replicant verkar lite i överkant för en hemsida.
Definitivt! Jag bara vet inget annat... Förutom cljd då. Vilket ser mer och mer attraktivt ut...
Behöver din hemsida en server?
Jag är förtjust i statiska, genererade, hemsidor. Christian och Magnars har gjort en del grejor för det också.
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.
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
Jag är också sjukt nyfiken på Datastar.
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.
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.
Datastar är här och nu. Men visst är det minblowing.
@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
Tacks, ska testa!
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.
En fördel med att använda något som nexus är att du kan dra nytta av andras erfarenheter med det
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
> 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.
Det är ett mindre problem i praktiken. Du skapar actions och effects som används enbart internt
Vi har som praxis att döpa allt i stilen effects.<widget>/<name>
Du definierar allt tillsammans med koden
Och importerar sedan allt till ett lokalt ställe