A simple, purely functional abstraction layer on top of Replicant inspired by the Elm architecture for Clojure and ClojureScript applications.
Version 0.1.0 is available for public testing and use. The documented API is the stable public contract for this release.
relm provides a clean, predictable, and functional approach to building user interfaces in Clojure/ClojureScript using the Elm Architecture pattern (Model-View-Update + Effects). It manages component lifecycles, isolated local states, global shared context, form management, and side effects while leveraging Replicant for lightweight, fast DOM rendering.
- Component-based Architecture: Independent components created via
(relm/component ...). - Hybrid State Management: Isolated local component state combined with a reactive global application context.
- Pure Functional Views: Pure view functions
(fn [state context] hiccup)that decouple rendering from state mutations. - Declarative Updates & Effects: Predictable message handlers (
relm/update) and asynchronous side-effect handlers (relm/fx). - Form State Management: Declarative validation, dirty/touch tracking, field registration, and semantic HTML attribute generation.
- Modular Ecosystem: Core runtime, forms, HTTP, navigation, query caching, and Reitit routing.
Add the required modules to your deps.edn:
{:deps {io.github.conjurernix/relm.core {:mvn/version "0.1.0"}
io.github.conjurernix/relm.devtools {:mvn/version "0.1.0"}
io.github.conjurernix/relm.form {:mvn/version "0.1.0"}
io.github.conjurernix/relm.query {:mvn/version "0.1.0"}
io.github.conjurernix/relm.reitit {:mvn/version "0.1.0"}}}For Leiningen / project.clj:
[io.github.conjurernix/relm.core "0.1.0"]
[io.github.conjurernix/relm.devtools "0.1.0"]
[io.github.conjurernix/relm.form "0.1.0"]
[io.github.conjurernix/relm.query "0.1.0"]
[io.github.conjurernix/relm.reitit "0.1.0"]| Module | Namespace | Description | Documentation |
|---|---|---|---|
| Core | relm.core |
Elm runtime, component lifecycle, state, update, and fx. |
Core Documentation |
| DevTools | relm.devtools |
Redux DevTools extension bridge, time-travel debugging, action history, and diffing. | DevTools Documentation |
| Form | relm.form |
Declarative form state, form/register, validators, and submission. |
Form Documentation |
| HTTP | relm.http |
Fetch API side effects (::fetch!, ::abort!) and JSON decoders. |
HTTP Documentation |
| Navigation | relm.navigation |
Browser History API effects (::push-state!, ::back!, etc.). |
Navigation Documentation |
| Query | relm.query |
TanStack Query-style caching, retries, optimistic mutations, and key inference. | Query Documentation |
| Reitit | relm.reitit |
Client-side routing with Reitit, context sync, and navigation messages. | Reitit Documentation |
Relm follows the Elm Architecture:
+--------------------------------+
| DOM Event |
+--------------------------------+
|
v
+--------------------------------+
| relm/dispatch |
+--------------------------------+
|
v
+--------------------------------+
| relm/update |
| [state context msg event] |
+--------------------------------+
/ | \
/ | \
v v v
+-----------+ +-------------+ +-------------+
| new-state | | new-context | | effects |
+-----------+ +-------------+ +-------------+
| | |
+-------+-------+ v
| +-------------+
v | relm/fx |
+----------------+ +-------------+
| Re-render DOM | |
| (Replicant) | v
+----------------+ (Async/Follow-up Msg)
- Model: Isolated local
stateper component instance + shared globalcontext. - View: Pure function
(fn [state context] hiccup)returning Replicant data structures. - Update: Pure multimethod
(update state context message event)returning[new-state new-context effects]. - Effects: Multimethod
(fx event effect)executing side effects linearly.
Define isolated components with pure initialization, rendering, and update handlers:
(defn init [_context {:keys [start] :or {start 0}}]
{:count start})
(defn view [{:keys [count]} _context]
[:div
[:span "Count: " count]
[:button {:on {:click [::increment]}} "+1"]])
(def Counter (relm/component {:init init :view view}))
(defmethod relm/update ::increment [state context _ _]
[(update state :count inc) context])Read full Core documentation ->
Colocate validation rules and initial values directly on fields with form/register:
(defn init [_context _]
{:form (form/create {:validate-on #{:change :blur :submit}})})
(defn view [{:keys [form]} _context]
[:form {:on {:submit (form/on-submit form {:on-submit [::save-user]})}}
[:input (form/register form :email {:type "email" :required "Email required" :email true})]
(when-let [err (form/error form :email true)]
[:span {:class "error"} err])
[:button {:type "submit" :disabled (form/submitting? form)} "Save"]])Read full Form documentation ->
Declarative HTTP Fetch requests with automatic JSON parsing and cancellation:
(defmethod relm/update ::fetch-user [state context [_ id] _]
[state context [[::relm.http/fetch!
{:url (str "https://api.example.com/users/" id)
:on-success [::user-loaded]
:on-failure [::user-failed]}]]])
(defmethod relm/update ::user-loaded [state context [_ {:keys [body]}]]
[(assoc state :user body) context])Read full HTTP documentation ->
Browser History API side effects:
(defmethod relm/update ::go-profile [state context [_ id] _]
[state context [[::nav/push-state! nil (str "/users/" id)]]])
(defmethod relm/update ::go-back [state context _ _]
[state context [[::nav/back!]]])Read full Navigation documentation ->
Declarative client-side routing with automatic context synchronization:
(def routes
[["/" {:name :home :view (fn [] [:h1 "Home"])}]
["/users" {:name :users :view (fn [] [:h1 "Users"])}]])
(defn root-view [_state context]
(let [view-fn (relm.reitit/current-view context)]
[:div
[:button {:on {:click [::relm.reitit/navigate-to "/"]}} "Home"]
[:button {:on {:click [::relm.reitit/navigate-to "/users"]}} "Users"]
(when view-fn (view-fn))]))Read full Reitit documentation ->
TanStack Query-style caching, flexible cache invalidation, retries, and optimistic mutations:
(ns my-app.posts
(:require [relm.query :as query]))
(def posts-key [:posts {:limit 10}])
(defn view [_state context]
(let [posts (query/data context posts-key [])
loading? (query/loading? context posts-key)
fetching? (query/fetching? context posts-key)]
[:div
[:button {:on {:click [::query/fetch posts-key
(query/key->opts posts-key {:stale-time 10000})]}}
(if fetching? "Fetching..." "Load Posts")]
(when loading? [:p "Loading..."])
[:ul (for [{:keys [id title]} posts]
[:li {:key id} title])]]))Read full Query documentation ->
| Dimension | relm |
re-frame |
|---|---|---|
| View Paradigm | Pure functions (fn [state context] hiccup) without reactive atoms |
Reagent components dereferencing reactive Reactions/RAtoms |
| DOM Renderer | Replicant (data-driven VDOM) | React via Reagent |
| State Model | Hybrid: Isolated local component state + shared global context | Single global store: app-db holds all app state |
| Event System | Pure relm/update multimethods returning [new-state new-context effects] |
Interceptor chain (reg-event-db / reg-event-fx) |
| Effect System | Ordered vector of effect vectors [[::fx ...]] |
Effect description maps {:fx [...]} |
| Dependencies | Minimal (Replicant only, zero React dependency) | React, Reagent, and re-frame |
| Dimension | relm |
Elm |
|---|---|---|
| Architecture | Model-View-Update + Side Effects ([state context effects]) |
Model-View-Update + Commands ((Model, Cmd Msg)) |
| Component Model | First-class hierarchical components with isolated local state | Monolithic model: Single root Model tree |
| View Paradigm | Pure functions returning Clojure data structures (Hiccup) | Pure functions returning typed Html Msg AST |
| Message System | Open multimethods (relm/update) on namespaced keywords |
Closed algebraic data types (type Msg = ...) |
| Side Effects | Extensible open multimethods (relm/fx) |
Opaque runtime Cmd / Sub with JS Ports |
# Run tests via tools.build
clojure -T:build test
# Or run tests via Shadow-CLJS directly
cd examples && npx shadow-cljs compile test && node out/test.jscd examples
npm install
npx shadow-cljs watch examplesThen open http://localhost:8080 in your browser.
Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version.