Liking cljdoc? Tell your friends :D

Nexus

noun
a means of connection; tie; link.

The UI is a pure function of application state — but event handling can be messy. It doesn’t have to be. Good event handling is declarative, minimal, and keeps side-effects well contained.

Nexus is a small, zero-dependency library for dispatching actions — data describing what should happen — with mostly pure functions.

no.cjohansen/nexus {:mvn/version "2026.06.2"}

Replicant provides a data-driven and functional solution to rendering. By making event handlers representable as data, it provides you with just enough infrastructure to build a declarative action dispatch system. Nexus is designed to be that system.

Table of contents

Nexus at a glance

Here's a compact showcase of using Nexus with Replicant. Read on for a detailed introduction to how it works.

(require '[nexus.registry :as nxr])
(require '[replicant.dom :as r])

(defn save [_ctx system path value]
  (swap! system assoc-in path value))

(defn increment [state path]
  [[:effects/save path (+ (:step state) (get-in state path))]])

(defn render [state]
  [:div
   [:p "Number: " (:number state)]
   [:div
    [:label "Step size: "]
    [:input
     {:value (:step state)
      :on
      {:input
       [[:effects/save [:step] [:fmt/number [:event.target/value]]]]}}]]
   [:button.btn
    {:on {:click [[:actions/inc [:number]]]}}
    "Count!"]])

;; Handle user input: register effects, actions and placeholders.
;; If you don't like registering these globally, the next section
;; shows how to use nexus.core, which has no implicit state.
(nxr/register-effect! :effects/save save)
(nxr/register-action! :actions/inc increment)

(nxr/register-placeholder! :event.target/value
  (fn [{:replicant/keys [dom-event]}]
    (some-> dom-event .-target .-value)))

(nxr/register-placeholder! :fmt/number
  (fn [_ value]
    (or (some-> value parse-long) 0)))

;; The `system`, a mutable application object
(def my-app (atom {}))

;; Specify how an immutable value is derived from the system
(nxr/register-system->state! deref)

;; Wire up the render loop
(r/set-dispatch! #(nxr/dispatch my-app %1 %2))
(add-watch my-app ::render #(r/render js/document.body (render %4)))

;; Trigger the initial render
(reset! my-app {:number 0, :step 1})

Getting started

Nexus actions are data structures that describe what your system should do.

Actions can originate from user events, timers, network responses, or other sources.

Actions are vectors of an action type (keyword) and optional arguments:

[:task/set-status "tid33" :status/in-progress]

When you dispatch an action in Nexus, it is handled by either:

  1. an action handler, a pure function that maps system state and the action's arguments to other actions.
  2. an effect handler, which performs side-effects according to:
    • a context map (we'll look at this later),
    • your system,
    • the action's arguments

If the action is handled by an effect handler, we call it an effect.

Nexus makes no assumptions about what the system is—you pass it when dispatching actions. In the example above, we use the my-app atom.

Implementing an effect handler

Let's consider a kanban task tracking example app.

Put effect handlers in your nexus map:

(def nexus
  {:nexus/effects
   {:task/start-editing
    (fn [_ system task-id]
      (swap! system assoc-in [:tasks task-id :task/editing?] true))}})

When the action is triggered, e.g. by a DOM event, dispatch it with the nexus map and your system:

(require '[nexus.core :as nexus])

(def kanban-app (atom {:tasks [,,,]}))
(nexus/dispatch nexus kanban-app {} [[:task/start-editing "tid33"]])

This separates what happens from how, but we can do better—by separating pure logic from effectful execution.

In Kanban, there are limits to how many tasks you can have in each column at the same time, meaning that the :task/set-status action is not just a mere assoc-in. This action needs to check how many tasks we already have with the desired status, and either update the task or flag an error:

(defn get-tasks-by-status [state status]
  (->> (vals (:tasks state))
       (filter (comp #{status} :task/status))))

(defn get-status-limit [state status]
  (get-in state [:columns status :column/limit]))

(def nexus
  {:nexus/effects
   {:task/start-editing
    (fn [_ system task-id]
      (swap! system assoc-in [:tasks task-id :task/editing?] true))

    :task/set-status
    (fn [_ system task-id status]
      (if (< (count (get-tasks-by-status @system status))
             (get-status-limit @system status))
        (swap! system assoc-in [:tasks task-id :task/status] status)
        (swap! system assoc :errors [:errors/at-limit status])))}})

Holy swap, Batman! That's a lot of side-effects in one place. Our goal is to isolate logic in pure functions, so we can test, reuse, and compose behavior without changing the world. Let's fix that.

Pure actions

We will first replace our two effect handlers with a more generic effect handler that updates the application state:

(def nexus
  {:nexus/effects
   {:effects/save
    (fn [_ctx system path v]
      (swap! system assoc-in path v))}})

Then our two domain-specific actions, :task/start-editing and :task/set-status, can now be expressed in terms of this one low-level effect.

We do that by implementing them as action handlers, which are pure functions that return a list of actions.

Action handlers are called with an immutable snapshot of your system, which means we need to tell Nexus how to acquire the system snapshot. Since our system is an atom, deref will do the job just fine:

(def nexus
  {:nexus/system->state deref ;; <==
   :nexus/effects {,,,}
   :nexus/actions             ;; <==
   {:task/start-editing
    (fn [state task-id]
      [[:effects/save [:tasks task-id :task/editing?] true]])

    :task/set-status
    (fn [state task-id status]
      (if (< (count (get-tasks-by-status state status))
             (get-status-limit state status))
        [[:effects/save [:tasks task-id :task/status] status]]
        [[:effects/save [:errors] [:errors/at-limit status]]]))}})

Now we also have clean separation between pure business logic and the side-effects. Your app will only ever need a handful of effect handlers; as your app grows you'll be adding action handlers. Nothing but pure functions all the way, baby!

It's also worth noting that the data the UI dispatches hasn't changed. We still dispatch something like

[:task/set-status "tid33" :status/in-progress]

but instead of a hard-to-test effect handler responding to this data, we now have an easier-to-test, pure action handler which responds to it by yielding an effect like

[:effects/save [:tasks "tid33" :task/status] :status/in-progress]

This is what allows you to start small and grow your system on demand, with very little boilerplate.

Using dispatch data

When using Replicant event handler data, you include actions in the rendered hiccup. However, some actions rely on data that isn't available until they dispatch.

Consider this action that updates the task title:

[:input
  {:placeholder "Task title"
   :name "task/title"
   :on {:input [[:task/update-title task-id ???]]}}]
                                            ^^^

To dispatch this we need the value from the input field at the time of dispatch. Placeholders solve this problem in a declarative way:

[:input
  {:placeholder "Task title"
   :name "task/title"
   :on {:input [[:task/update-title task-id [:event.target/value]]]}}]

[:event.target/value] is a placeholder to be resolved during dispatch. Placeholders are implemented by the keyword:

(def nexus
  {:nexus/system->state deref
   :nexus/effects {,,,}
   :nexus/actions {,,,}
   :nexus/placeholders        ;; <==
   {:event.target/value
    (fn [dispatch-data]
      (some-> dispatch-data :dom-event .-target .-value))}})

Where does dispatch-data come from? It is the third argument to nexus.core/dispatch:

(nexus/dispatch nexus system {:dom-event ,,,}
 [[:task/update-title "tid33" [:event.target/value]]])

You can also embed dispatch data in the state snapshot by replacing :nexus/system->state with :nexus/system+dispatch-data->state. This function will receive both the system and dispatch data, and is expected to return an immutable state snapshot.

Calling event methods

Dispatch data is also available to effect handlers. Let's say we have a form to edit the task. Instead of controlling each input, it will use the form submit action. To avoid a page refresh on submit we must call .preventDefault on the event:

[:form
 {:on
  {:submit
   [[:effects/prevent-default]
    [:task/edit task-id [:event.target/form-data]]]}}
 ,,,]

Effect handlers receive dispatch-data as part of their first argument, the aforementioned context map:

(def nexus
  {,,,
   :nexus/effects
   {,,,
    :effects/prevent-default
    (fn [{:keys [dispatch-data]} _]
      (some-> dispatch-data :dom-event .preventDefault))}})

Clock time

You may want to record the time of the last edit. However, getting the current time in the action handler means it's no longer pure (and will make it much harder to test). You can solve this by passing in the current time.

One way to achieve this is to use another placeholder:

(def nexus
  {,,,
   :nexus/placeholders
   {,,,
    :clock/now
    (fn [{:keys [now]}]
      now)}})

(nexus/dispatch nexus system {:dom-event ,,,
                             :now (js/Date.)}
 [[:task/update-title "tid33" [:event.target/value] [:clock/now]]])

Note that you could write this placeholder very succinctly:

(def nexus
  {:nexus/placeholders
   {:clock/now :now}})

Another option is to make sure the state always has the current time on it:

(def nexus
  {,,,
   :nexus/system->state
   (fn [system]
     (assoc @system :clock/now (js/Date.)))
   :nexus/actions
   {:task/edit
    (fn [state task-id data]
      (into [[:effects/save [:tasks task-id :task/updated-at] (:clock/now state)]]
            (for [[k v] data]
              [:effects/save [:tasks task-id k] v])))}})

Nested placeholders

You may have wondered why the placeholder keyword is wrapped in a vector: [:event.target/value]. The vector allows placeholders to nest, so you can transform late bound values without having to litter your code with things like parse-long, or duplicate placeholders, e.g. :event.target/value-as-*.

Let's say you wanted to get a number from an input field:

[:input
 {:on
  {:input
   [[:task/set-order task-id [:fmt/long [:event.target/value]]]]}}]

The :fmt/long placeholder takes an argument, which is passed to the placeholder function:

(def nexus
  {,,,
   :nexus/placeholders
   {:event.target/value
    (fn [{:keys [dom-event]}]
      (some-> dom-event .-target .-value))

    :fmt/long
    (fn [_ val]
      (or (some-> val parse-long) 0))}})

Opting out of placeholder interpolation

If you have actions with large data structures in them that should never contain placeholders, you can opt out of interpolation for some performance benefits by adding meta data:

[[:task/set-order
  (with-meta huge-task {:nexus/skip-interpolation true})
  [:fmt/long [:event.target/value]]]]

You may want to consider not putting enormous data structures in actions first, but for those cases where it can't be avoided, this escape hatch is available.

Most of the time, adding ^:nexus/skip-interpolation is not necessary. Measure before you optimize.

Batching effects

Batching is deprecated, see the dedicated document for details.

Asynchronous effects

Imagine that there is a form to add new tasks. When the form is submitted, we want to issue a command to the server to create a new task, and redirect the user to a dedicated page for it.

Here's an effect handler to send a command to the server:

(def nexus
  {:nexus/effects
   {:effects/command
    (fn [ctx system command]
      (js/fetch "/commands"
                #js {:method "POST"
                     :body (pr-str command)}))}})

To redirect the user, we need to dispatch a new effect when the command completes, looking up the new location from the response. The ctx passed to effect handlers includes a :dispatch function. It lets you trigger new effects with access to the same nexus, system and dispatch-data.

(def nexus
  {:nexus/effects
   {:effects/command
    (fn [{:keys [dispatch]} system command]
      (-> (js/fetch "/commands"
                    #js {:method "POST"
                         :body (pr-str command)})
          (.then
           (fn [response]
             (dispatch
              [[:effects/navigate
                (.get (.-headers response) "Location")]])))))}})

This works, but it couples concerns more tightly than we'd like. We want as few effects as possible, meaning that they should be as general as possible. We can't assume that every command should result in a navigation. Instead we want to make this decision when issuing the command:

(nexus/dispatch nexus system dispatch-data
 [[:effects/command
   {:command/kind :commands/create-task
    :command/data
    {:task/title "Learn Nexus"
     :task/priority :task.priority/high}}
   {:on-success [[:effects/navigate ???]]}]])
                                    ^^^

Once again, we want to refer to a value that’s only available when the effect is triggered—without writing imperative glue code. Unfortunately we can't just stick a placeholder here, as it would resolved immediately upon dispatching :effects/command -- there is no way for Nexus to know that :on-success describes something that should happen later.

This problem needs a custom solution in your app. One possible solution is to use a placeholder anyway, and implement it such that it preserves the placeholder when there is not yet a value to replace it with:

(def nexus
  {:nexus/placeholders
   {:http.res/header
    (fn [{:keys [response]} header]
      (if response
        (.get (.-headers response) header)
        [:http.res/header header]))}})

We can use the placeholder in the :on-success effects:

[[:effects/command
  {:command/kind :commands/create-task
   :command/data
   {:task/title "Learn Nexus"
    :task/priority :task.priority/high}}
  {:on-success [[:effects/navigate [:http.res/header "Location"]]]}]]

Finally, in the command effect handler, we can provide additional dispatch data when dispatching new effects. This dispatch data will be merged into the original dispatch data:

(def nexus
  {:nexus/placeholders
   {:http.res/header
    (fn [{:keys [response]} header]
      (.get (.-headers response) header))}

   :nexus/effects
   {,,,
    :effects/command
    (fn [{:keys [dispatch]} system command {:keys [on-success]}]
      (-> (js/fetch "/commands"
                    #js {:method "POST"
                         :body (pr-str command)})
          (.then
           (fn [response]
             (when on-success
               (dispatch on-success {:response response}))))))}})

The effect handler now accepts a fourth argument that matches the extra options in the dispatched effect:

[:effects/command command opts]

For a production-ready setup you should also support an :on-failure option in the last map.

With this setup, we’ve cleanly separated concerns: the command effect remains generic, while callers can declaratively define what should happen on success. This pattern generalizes well and can be used to implement all kinds of asynchronous flows.

Putting it together

If you're rendering with Replicant, you can introduce it to Nexus with a one-liner:

(require '[nexus.core :as nexus]
         '[replicant.dom :as r])

(def nexus ,,,)
(def system (atom {}))

(defn state->ui-data [state]
  [:div
   [:h1 "Hello world!"]])

(defn start [el nexus system]
  ;; Dispatch Replicant's event data with Nexus
  (r/set-dispatch! #(nexus/dispatch nexus system %1 %2))
  (add-watch system ::render #(r/render el (state->ui-data %4)))
  (swap! system assoc ::started-at (js/Date.)))

Replicant calls the function passed to set-dispatch! with a map that contains, among other things, the DOM event under the key :replicant/dom-event.

Beware that this approach can cause multiple renders for a single dispatch if there are more than one effect that swaps on the system atom. To ensure each dispatch only results in a single render (which will give you the best rendering performance), you have a few options.

Rendering from an interceptor

If you only need to trigger rendering from a Nexus dispatch, you can use an interceptor:

(defn start [el nexus system]
  (let [nexus (update nexus :nexus/interceptors conj
                      {:after-dispatch
                       (fn [ctx]
                         (r/render el (state->ui-data @system))
                         ctx)})]
    (r/set-dispatch! #(nexus/dispatch nexus system %1 %2))
    (swap! system assoc ::started-at (js/Date.))
    (r/render el (state->ui-data @system))))

This will trigger a render for every dispatch, including async ones triggered by effects. However, swap!-ing on the atom from elsewhere will not trigger a render.

Using a render lock

If you'd rather tie rendering to updates to the system atom, you can implement a render lock to avoid multiple renders per dispatch. The render lock is controlled from an interceptor to ensure that it is also in place for async dispatch calls triggered by effects:

(defn render [el state]
  (when-not (:pause-rendering? state)
    (r/render el (state->ui-data state))))

(defn start [el nexus system]
  (let [nexus (update nexus :nexus/interceptors conj
                      {:before-dispatch
                       (fn [ctx]
                         (swap! system assoc :pause-rendering? true)
                         ctx)

                       :after-dispatch
                       (fn [ctx]
                         (swap! system dissoc :pause-rendering?)
                         ctx)})]
    (r/set-dispatch! #(nexus/dispatch nexus system %1 %2))
    (add-watch system ::render #(render el %4))
    (swap! system assoc ::started-at (js/Date.))))

Batching effects

By batching the effects that swap on the system, you ensure that each dispatch only results in at most one swap, thus one render. However, this approach is discouraged and deprecated, see the batching document.

Development tooling

...if only you could see what I've seen with your eyes

If you are using Dataspex you can load Nexus' custom action panel like so:

(require '[nexus.action-log :as action-log])

(defn inspect-actions [nexus]
  (let [log (action-log/create-log)]
    (action-log/install-inspector log)
    (action-log/install-logger nexus log)))

...and use the returned nexus with nexus/dispatch.

Actions in the Dataspex action log Details about one dispatched action

(nexus.action-log/create-log & [opt])

You can pass some options to this function to customize the log:

  • :slow-threshold The number of milliseconds used to determine if a dispatch was slow. Defaults to 100.
  • :max-entries The maximum number of entries to render. Will hide the oldest dispatches. Setting this to a reasonable number will keep the inspector snappy.
  • :max-age A map of {:seconds :minutes :hours :days} that determines how old an action should be before it is no longer rendered in the log. For instance, (action-log/create-log {:max-age {:hours 1}}) will only render actions from the past hour. Can be used in combination with :max-entries.

Give me convenience, or give me death

Compiling the nexus map and passing it by hand makes things explicit at the cost of some boilerplate on your end. Maybe you're willing to swallow a little implicitness for a lot of convenience? Then nexus.registry is for you.

nexus.registry provides some wrappers that collect effect handlers, action handlers, and placeholders in a global registry:

(require '[nexus.registry :as nxr])

(nxr/register-effect! :effects/save
  ^:nexus/batch
  (fn [_ctx system path-vs]
    (swap! system
     (fn [state]
       (reduce (fn [acc [path v]]
                 (assoc-in acc path v))
               state path-vs)))))

(nxr/register-placeholder! :event.target/value
  (fn [{:replicant/keys [dom-event]}]
    (some-> dom-event .-target .-value)))

(nxr/register-placeholder! :fmt/number
  (fn [_ val]
    (or (some-> val parse-long) 0)))

(nxr/register-action! :task/start-editing
  (fn [state task-id]
    [[:effects/save [:tasks task-id :task/editing?] true]]))

(nxr/register-action! :task/set-status
  (fn [state task-id status]
    (if (< (count (get-tasks-by-status state status))
           (get-status-limit state status))
      [[:effects/save [:tasks task-id :task/status] status]]
      [[:effects/save [:errors] [:errors/at-limit status]]])))

,,,

nexus.registry/dispatch implicitly uses this registry:

(replicant.dom/set-dispatch!
 (fn [dispatch-data actions]
   (nxr/dispatch system dispatch-data actions)))

Using the global registry makes the dev setup much easier, because you don't need to coordinate on the nexus map. Just add this to your development setup:

(require '[nexus.action-log :as action-log])

(action-log/inspect {:max-age {:hours 3}})

See above for details on the optional map passed to inspect.

Rationale

Tutorials on the Replicant website demonstrate that rolling your own action dispatch system is not a big undertaking. So why does this library exist?

Lower barrier to entry

Designing a robust action dispatch system can be daunting when you’re just getting started. Even if the pieces are conceptually simple, it takes time and experience to get the architecture right, especially in a functional setting.

We’ve spent over a decade building systems like this in Clojure. Nexus is our distilled take on how to do it cleanly and consistently, so you don’t have to reinvent the wheel or get stuck figuring it out on day one.

Even for experienced developers, it’s easy to cut corners when wiring up yet another dispatch system—skipping instrumentation, half-assing some features, or going light on error handling. Nexus takes care of those finer points once and for all.

Standardized data flow

Every app needs some kind of effect dispatch system. Even if we all agree that an effect is a vector with a keyword identifier and some arguments, there are many ways to build the dispatch pipeline. There’s little value in making it slightly different in every project—but doing so does come with a cognitive cost.

You need some action system from the very beginning—often before you know if you’ll need to separate actions from side-effects, if actions will need to trigger other actions (maybe asynchronously), or if you’ll want placeholder interpolation.

If we can build a strong, flexible system for this flow in a small package, it can help reduce decision fatigue and free up your mental bandwidth to focus on your domain instead of your wiring.

Reduced boilerplate

Sure, a basic dispatch system isn’t much code—but it is code you’ll end up copying between projects. With Replicant and Nexus together, you get up and running with just a handful of lines. That means less setup, fewer errors, and more consistency across projects.

Strong developer tooling

Action dispatch is one of those areas where good observability is a genuine superpower. Nexus ships with built-in integration for Dataspex, letting you inspect dispatch flows live during development.

It also exposes a fine-grained interceptor API you can extend however you want—add production tracing, connect it to custom tooling, or build your own time-travel debugger.


We’re not trying to scare you into using a library when 50 lines of code could work. But for ~160 lines, Nexus gives you a clean, tested, extensible dispatch system with excellent dev tooling built in.

Nomenclature

Finding the right nomenclature was the most important part of the Nexus design process. Strong names empower you to build a system with the right focus, help you reach for the right tools for the job, and lightens the learning curve.

Action

A vector beginning with a keyword (e.g. [:auth/login "alice" "secret"]) that represents an instruction to the system. The keyword identifies the action type, and any following values are action arguments.

Actions were deliberately not named "events" for two reasons:

  1. Events describe something that occurred that the system may or may not want to react to. This is much too indirect for describing the result of clicking a button. When clicking a button we don't have a "clicked login-button" event, we have a "login" action - a clear instruction to the system.
  2. The word "event" is already heavily loaded in the context of a browser. In fact, actions typically will be dispatched in reaction to a DOM event. Having multiple different artifacts that are all named event does not make writing code easier.

Action handlers are pure functions that take the state (see below) and any arguments from the action, and return a list of actions/effects.

Effect

When an action reaches an effect handler to be processed for its side-effects, we call it an effect.

An effect handler is a function that receives ctx, the live system and any action arguments and uses them to perform side-effects:

[:effect/save [:number] 3]

(fn [ctx system path value])

Here path would be bound to [:number] and value to 3.

When an effect handler is batched, it receives a collection of arguments instead:

[[:effect/save [:number] 3]
 [:effect/save [:name] "Nexus"]]

^:nexus/batch
(fn [ctx system path-values])

In this case path-values would be:

[[[:number] 3]
 [[:name] "Nexus"]]

System

The system contains your live, mutable application state and services. Nexus treats this as an opaque value—it never inspects or modifies it. It is passed to effects (to perform work) and to your :nexus/system->state or :nexus/system+dispatch-data->state function (to extract pure data for action handlers).

State

The result of calling :nexus/system->state or :nexus/system+dispatch-data->state on your system. The result is assumed to be immutable data.

Dispatch data

Data that is only available at the time an action is dispatched. Prime examples include the DOM event that triggered the action or the target element, but can be anything a pure action handler wouldn't otherwise have access to, like the current time.

Placeholder

Placeholders allow actions to refer to data that isn't available until the action is dispatched. They are represented as a vector with a keyword, and possibly additional arguments. Placeholders have an implementation associated with the first keyword.

nexus

A map defining your Nexus system. It includes keys like :nexus/actions, :nexus/effects, :nexus/placeholders, :nexus/interceptors, and :nexus/system->state. This is the central registry passed to nexus.core/dispatch.

When using nexus.registry, Nexus maintains this registry for you.

ctx

The context map passed to interceptors. It contains data relevant to the current interceptor phase (e.g. :action, :errors, :results) and data about other interceptors (:queue/:stack). Interceptors can read or modify this context to influence execution.

Error handling

Nexus catches errors from handlers and interceptors, and propagates them as data. By default, a dispatch can be partially successful—some actions may complete while others fail in an action or effect handler. You can control this behavior with interceptors. Nexus ships with an opt-in fail fast strategy if you'd like all action and effect processing to halt on the first error:

(require '[nexus.strategies :as strategies])

(def nexus
  {:nexus/interceptors [strategies/fail-fast]
   :nexus/actions ,,,
   :nexus/effects ,,,})

If you're using the registry:

(require '[nexus.registry :as nxr])
(require '[nexus.strategies :as strategies])

(nxr/register-interceptor! strategies/fail-fast)

With the fail-fast strategy, action and effect processing stops at the first failure. dispatch itself will never throw. Errors are collected and returned in the :errors key. If you want an exception thrown on error, you can do that after calling dispatch:

(require '[nexus.core :as nexus])

(let [{:keys [results errors]} (nexus/dispatch nexus actions)]
  (when-let [error (->> errors (keep :err) first)]
    (throw error)))

Note that this will only throw the first of potentially several errors.

Interceptors

Interceptors let you instrument and control the flow of actions. They can run before or after dispatch, action handlers, and effect handlers.

An interceptor is a map with an :id and any number of phase functions, keyed by:

  • :before-dispatch
  • :after-dispatch
  • :before-action
  • :after-action
  • :before-effect
  • :after-effect

All interceptor functions take a single argument, ctx, and must return an updated ctx. Its contents vary by phase, but always include:

  • :queue — the remaining before-phase interceptors. The handler function appears at the end of this list.
  • :stack — the after-phase interceptors to run once the handler completes.

By modifying :queue or :stack, interceptors can short-circuit processing or skip specific interceptors.

Specific arguments passed to the various phase functions:

:before-dispatch

  • :system
  • :state
  • :dispatch-data
  • :actions

:after-dispatch

  • :system
  • :state
  • :dispatch-data
  • :actions
  • :results
  • :errors

:before-action

:before-action is only called for actions that have a registered action handler. Any action that only has an effect handler will not be visible with this interceptor.

  • :state
  • :action
  • :errors (errors from previous action handlers)

:after-action

:after-action is called after an action is expanded, and each resulting action/effect has been expanded and executed.

  • :state
  • :actions
  • :errors (errors from previous action handlers)

:before-effect

  • :system
  • :dispatch-data
  • :dispatch — a function to dispatch new actions with the same nexus, system, and dispatch-data.
  • :effect or :effects (batched effect handler)
  • :errors (errors from previous effect handlers)
  • :results (from previous effects)

:after-effect

  • :system
  • :dispatch-data
  • :dispatch
  • :effect or :effects (batched effect handler)
  • :errors
  • :results

Example: logging

Here's an example of an interceptor that logs actions as they're processed as effects:

(def logger
  {:id :logger

   :before-action
   (fn [{:keys [action] :as ctx}]
     (println "Expanding action:\n" (pr-str action))
     ctx)

   :after-action
   (fn [{:keys [errors] :as ctx}]
     (when (seq errors)
       (println "⚠️ Error while expanding action!")
       (prn errors))
     ctx)

   :before-effect
   (fn [{:keys [effect effects] :as ctx}]
     (if effect
       (println "Executing effect:\n" (pr-str effect))
       (println "Executing batched effects:\n" (pr-str effects)))
     ctx)

   :after-effect
   (fn [{:keys [errors] :as ctx}]
     (when (seq errors)
       (println "⚠️ Error while executing effect!")
       (prn errors))
     ctx)})

(def nexus
  {:nexus/interceptors [logger]
   :nexus/actions ,,,
   :nexus/effects ,,,})

You can also register interceptors with nexus.registry/register-interceptor!.

Error callback

You can register a function to receive any error Nexus encounters. The function will be called with two arguments: the interceptor ctx map, and a map describing the error. Register this function as :nexus/on-error in your Nexus map, or with (nexus.registry/on-error (fn [ctx error] ,,,)).

Nexus in the wild

Ring middleware: ring-nexus-middleware applies Nexus to Ring handlers for clean, testable web applications.

Datastar: datastar.wow uses Nexus to create extensible, testable, and data-oriented real-time applications via Datastar.

Acknowledgments

Original design by Magnar Sveen (@magnars), Christian Johansen (@cjohansen), and Teodor Heggelund (@teodorlu).

Changelog

2026.06.2

Change execution model to eagerly execute commands, see relevant ADR.

Thanks Cormac Cannon!

Deprecate batching, and move it to an opt-in interceptor. See section on batching above.

Remove the error propagation for synchronous nested calls to dispatch introduced in 2025.11.1. This change duplicated errors in interceptors, and does not work consistently for async and sync calls to dispatch.

Include a :trace with errors that includes every action and effect that lead to an error. This gives full traceability across nested dispatch calls for both async and sync calls, and can be leveraged by tooling to properly "connect the dots" when errors occur.

Add :nexus/system+dispatch-data->state as an alternative to :nexus/system->state.

Add ^:nexus/skip-interpolation to skip interpolation of large data structures in action data.

Add :nexus/on-error callback to observe every error occurring during dispatch.

2025.11.1

Fix a bug where nested calls to dispatch would swallow errors. Synchronous errors are now propagated to the root call. Thanks Martin Solli!

2025.10.2

Fixed a bug with interpolation accidentally introduced in 2025.10.1 (Martin Solli).

2025.10.1

Interpolate placeholders in between each action expansion.

License: MIT

Copyright © 2025-2026 Christian Johansen, Magnar Sveen, and Teodor Heggelund. Distributed under the MIT License.

Nexus is software written for and by humans.

Can you improve this documentation? These fine people already did:
Christian Johansen, Kevin J. Lynagh, Ovi Stoica, Grigoriy Beziuk, Martin Solli, Brian Scaturro & Anders Furseth
Edit on GitHub

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