npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

reagami

v0.2.40

Published

Readme

Reagami

npm

Fold your state into the DOM!

A minimal zero-deps Reagent-like in Squint and CLJS.

Usage

Quickstart example:

(ns my-app
  (:require ["https://esm.sh/reagami" :as reagami]))

(def state (atom {:counter 0}))

(defn my-component []
  [:div
   [:div "Counted: " (:counter @state)]
   [:button {:on-click #(swap! state update :counter inc)}
    "Click me!"]])

(defn render []
  (reagami/render (js/document.querySelector "#app") [my-component]))

(add-watch state ::render (fn [_ _ _ _]
                            (render)))

(render)

(Open this example on the Squint playground)

In ClojureScript you would add this library to your deps.edn :deps as follows:

io.github.borkdude/reagami {:git/sha "<latest-sha>" :git/tag "<latest-tag>"}

and then require it with (:require [reagami.core :as reagami]).

To start a new project, run:

npm create reagami-app my-app

The command creates a Vite project with hot reload and a browser nREPL. See create-reagami-app.

Reagami supports:

  • Building small reactive apps with the only dependency being Squint or CLJS. Smallest app with Squint after minification is around 5.5 KB gzip.
  • Rendering hiccup into a container DOM node. The only public function is render.
  • Event handlers via :on-click, :on-input, etc.
  • Default attributes: :default-value, etc. for uncontrolled components
  • Id and class short notation: [:div#foo.class1.class2]
  • Disabling properties with false: [:button {:disabled (not true)}]
  • :style maps: {:style {:background-color :green}}
  • :on-render hook. See docs here.
  • Keyed children for better diffing via :key. See docs here.
  • Server-side rendering with hydration via reagami.ssr, on the JVM, Babashka, Squint, and CLJS. See Server-side rendering.

Reagami does NOT support:

  • Auto-rerendering by auto-watching custom atoms. Instead you use add-watch + render on regular atoms or you call render yourself.
  • React hooks (it doesn't use React)
  • Repairing changes made by other code. Reagami owns the children of the nodes it renders. A node that something else adds, removes or reorders stays that way.

Local state can be accomplished by using nested renders like in this example or using web components.

Reagami's patching algorithm is detailed in the Patch algorithm section.

For a more fully featured version of Reagent in squint, check out Eucalypt.

:on-render

Reagami calls the :on-render hook after it mounts, updates or unmounts a DOM node. The hook takes a map:

[:div {:on-render (fn [{:keys [node lifecycle state save]}] ...)}]
  • node: the DOM node.
  • lifecycle: one of :mount, :update or :unmount.
  • state: the value that you saved on the last call. It is nil on :mount.
  • save: call it with one value to keep that value for the next call.

Example:

(fn [{:keys [node lifecycle state save]}]
  (case lifecycle
    :mount
    (save {:stop (install-clock! node)
           :updates 0})

    :update
    (save (update state :updates inc))

    :unmount
    (do
      (println "Number of updates in total:" (:updates state))
      ((:stop state)))))

On :unmount the node is already out of the document.

Keyed children

You can add a :key property to your elements to identify nodes. This will result in better performance when Reagami re-renders.

[:ul
 (for [{:keys [id label]} items]
   [:li {:key id} label])]

Server-side rendering

reagami.ssr renders hiccup to an HTML string. It runs on the JVM, on Babashka, on Squint, and on ClojureScript. The namespace has one public function: render.

(require '[reagami.ssr :as ssr])

(ssr/render [:div#app [:p "Hello"]])
;;=> "<div id=\"app\"><p>Hello</p></div>"

To make hydration work, follow these steps:

  1. On the server, render the page with ssr/render. Put the result in the container element.
  2. On the client, call reagami.core/render with that container and the same hiccup.

The ssr example shows a small server on Babashka with hydration on the client. The ssr-live example adds server state, event streams, and edits. A live instance of that example runs at https://reagami-ssr-live.michielborkent.nl.

The output follows the hydration contract:

  • The output contains no :key attributes and no event handlers. The client adds the handlers.
  • A nil child becomes the empty comment <!---->. This comment keeps the position of the child.
  • Numbers get the JavaScript format. The double 1.0 becomes the string "1".
  • The value of :innerHTML goes into the output without a change.
  • A property with no HTML form, such as :indeterminate, :volume and :playbackRate, is left out. The client sets it on the first render.

CAUTION: Do not put user input in tag names, in attribute names, or in :innerHTML. Reagami escapes text and attribute values only.

Patch algorithm

The patch algorithm works as follows. When re-rendering elements on the screen, Reagami compares them with the previous corresponding elements.

patch-node

For a single node, patch-node decides reuse of an existing node or to create one from scratch. Two text nodes reuse the old node and update its text. Two elements with the same tag reuse the old node, sync its attributes and recurse into its children. In other situations a new node is created and the old one is discarded. When a node is reused, the children must also be reconciled. Reagami first checks whether any child has a :key. If at least one keyed child is present, the keyed algorithm is used, else the unkeyed (positional) algorithm.

Unkeyed

The unkeyed algorithm matches children by position.

  1. First the shared prefix is determined: the leading positions present in both lists, that is the first min(old-count, new-count) children. For old [a b c] and new [a b] that is the first two positions, and for old [a b] and new [a b c] also the first two. The prefix is positional, the nodes at those positions need not match.
  2. The shared prefix is patched index-wise using patch-node. At each index patch-node is applied to the corresponding elements. See above for how patch-node works.
  3. After that, there can be two cases to handle: a. There are more new children than old: extra new nodes are created + appended. b. There are more old children than new: extra old nodes are removed. One special case of this is that there are 0 new children in total, so all old children must be removed. Reagami then uses parent.textContent = "" as an optimization.

Example: old [a b c], new [d e]. Positions 1 and 2 overlap, so a is patched toward d and b toward e using patch-node. The same tag means the node is reused and updated, a different tag means the new node replaces the old. Position 3 (c) is removed.

The unkeyed algorithm doesn't move any nodes, so expensive collapses can happen, e.g. when a new node must be inserted at or near the front. In a situation where extra performance is needed, add :keys so the keyed algorithm will be used, which can reliably move nodes around.

Keyed

The keyed algorithm is inspired by Vue3 and uses the Longest Increasing Subsequence algorithm.

When any child has a :key, the whole list is reconciled by key. Keyed and unkeyed children can be mixed, although it's recommended to use keys on all the elements. The unkeyed elements are matched positionally. The algorithm is best shown with an example. Below, a plain letter is a keyed node whose key is that letter, and a parenthesised letter like (u) is an unkeyed node.

Let's say we have the following situation:

old:  a    b    c    d    z    (u)
new:  a    d    b    c    (u)  n    (m)
  1. Match each new child to an old node and note that old node's position. Positions are 1-based, since 0 marks a new node. Each matched old node is reused and patched toward its new vnode with patch-node (attributes and children updated, recursively). A new child with no match is built with create-node.
a    ->  old a    pos 1    by key
d    ->  old d    pos 4    by key
b    ->  old b    pos 2    by key
c    ->  old c    pos 3    by key
(u)  ->  old (u)  pos 6    next unused unkeyed old, taken in order
n    ->  create   pos 0    no old node with key n
(m)  ->  create   pos 0    no unkeyed old left

old positions (in new order) = [1 4 2 3 6 0 0]
  1. Remove old nodes that weren't matched. z was not matched, so it is removed.

  2. Find the longest increasing subsequence of the old positions, skipping the 0 holes. This is the largest set of nodes already in the right relative order. Relative here means: there can be other positions in between. In 1 4 2 3 6 _ _ the longest increasing subsequence is 1 2 3 6: that is a, b, c and (u). The 4 (node d) is left out. These four will not move.

  3. Place nodes right to left into the parent, moving only the ones outside that subsequence. The DOM can only insert a node before a reference node (insertBefore, there is no insertAfter), so each node is anchored on its right neighbour. Therefore we iterate from right to left to ensure the right neighbour is already in place. The rightmost node has no right neighbour, so its reference is null. parent.insertBefore(node, null) is identical to appendChild: with no node to go before, it goes at the end.

(m)  new             ->  append (insertBefore null)
n    new             ->  insertBefore (m)
(u)  in subsequence  ->  leave in place
c    in subsequence  ->  leave in place
b    in subsequence  ->  leave in place
d    reused, moved   ->  insertBefore b
a    in subsequence  ->  leave in place

Result: a d b c (u) n (m), with z removed. Only d was moved, n and (m) were created, and the rest never moved.

Benchmarks

In the below benchmarks, Reagami is compared against other CLJS UI libraries with js-framework-benchmark and used the keyed variant. Reagent, Helix and UIX are tested with React 19.2. A more detailed explanation of the methodology and how you can run it yourself are in doc/benchmarks.md.

Geometric mean across the nine keyed table operations (lower is better):

---
config:
  xyChart:
    width: 850
    height: 480
  themeVariables:
    xyChart:
      plotColorPalette: "#ff7f0e, #4c78a8"
---
xychart-beta
    title "Perf: geomean of 9 keyed ops (ms, lower is better)"
    x-axis ["UIX", "Reagami Squint", "Helix", "Reagami CLJS", "Reagent", "Replicant CLJS", "Replicant Squint"]
    y-axis "ms" 0 --> 60
    bar [-5, 34.0, -5, 38.0, -5, -5, -5]
    bar [32.8, -5, 36.7, -5, 40.9, 45.9, 52.1]

The same data-table app was compiled with production settings. Below we compare the output size, gzipped.

---
config:
  xyChart:
    width: 850
    height: 480
  themeVariables:
    xyChart:
      plotColorPalette: "#ff7f0e, #4c78a8"
---
xychart-beta
    title "Bundle size (gzip KB, lower is better)"
    x-axis ["Reagami Squint", "Replicant Squint", "Reagami CLJS", "Replicant CLJS", "UIX", "Helix", "Reagent"]
    y-axis "KB" 0 --> 100
    bar [9.4, -5, 28.5, -5, -5, -5, -5]
    bar [-5, 16.5, -5, 40.2, 89.5, 96.1, 97.1]

The minimal Reagami app under Squint is smaller, around 5.5 KB gzip, but in the benchmark the js-framework-benchmark's standard table app is compared.

Reagami on Squint is in the same range as the React libraries and the React-free alternatives. It has the smallest output size of the group. The Squint target is faster than the CLJS target.

Examples

Examples on the Squint playground:

The web component example builds a <todo-list> custom element and uses it from Squint, from JavaScript and without Reagami.

License

MIT