HSX is a ClojureScript library for writing React components using Hiccup syntax. We believe Hiccup is the most idiomatic (and joyful) way to express HTML in Clojure.
Think of HSX as a lightweight syntactic layer over React, much like JSX in the JavaScript world.
HSX is designed to offer a seamless transition from Reagent to plain, idiomatic React Function components. It’s compatible with Reagent-style Hiccup, making it trivial to migrate your existing codebase to HSX.
Unlike Reagent, HSX does not:
If you want to read more about the engineering challenge of moving a 60k+ LOC Reagent codebase to React 19 read this blog post.
Reagent was ahead of its time, giving ClojureScript developers advanced tools like reactive atoms and declarative UI rendering. However, React has since evolved, and modern React features such as hooks and concurrent rendering are fundamentally incompatible with Reagent’s internals.
Using HSX is straightforward. The entire library is only about 300 lines of ClojureScript (with comments). HSX is designed to be as close to plain React as possible while retaining the expressive power of Hiccup and Clojure data structures.
HSX exposes two primary functions:
io.factorhouse.hsx.core/create-element
- like react/createElement
but for HSX componentsio.factorhouse.hsx.core/reactify-component
- like reagent.core/reactify-component
(ns com.corp.my-hsx-ui
(:require [io.factorhouse.hsx.core :as hsx]
["react-dom/client" :refer [createRoot]]))
;; This is a HSX component
(defn test-ui [props text]
[:div props
"Hello " text "!"])
(defonce root
(createRoot (.getElementById js/document "app")))
(defn init []
(.render root
(hsx/create-element
[test-ui {:on-click #(js/alert "Clicked!")}
"prospective HSX user"])))
See the examples directory for more examples.
If you have an existing Reagent codebase, the following reagent.core
functions map to:
reagent.core/as-element
-> io.factorhouse.hsx.core/create-element
reagent.core/reactify-component
-> io.factorhouse.hsx.core/reactify-component
reagent.core/create-element
-> react/createElement
When migrating from Reagent you will objectively find performance wins for your application by:
:f>
) and improved interop with React libraries.When profiling our real-world, enterprise grade product (Kpow) we saw 4x fewer commits without the overall render duration blowing out after switching to HSX. More details here.
HSX components are just React function components under the hood with a bit of syntactic sugar.
There are no state abstractions found in this library. We suggest you migrate any Reagent components with local state to use react/useState
. The useState
hook is the most idiomatic way to deal with local state in React.
;; (:require ["react" :as react])
(defn reagent-component-with-local-state []
(let [state (reagent.core/atom 1)]
(fn []
[:div {:on-click #(swap! state inc)}
"The value of state is " @state])))
(defn hsx-component-with-local-state []
(let [[state set-state] (react/useState 1)]
[:div {:on-click #(set-state inc)}
"The value of state is " state]))
We have a companion library named RFX which is a drop-in replacement for re-frame without the dependency on Reagent.
See the RFX repo for more details.
If RFX is overkill for your application (or you have bespoke requirements), you can use standard React solutions for global application state management like:
Using shadow-cljs add a reload function like:
(defn ^:dev/after-load reload []
(hsx/memo-clear!))
This will clear the component cache after a code change.
Exactly the same as Reagent:
[:div {:on-click #(js/alert "Clicked!")}]
Would translate to:
[:div #js {"onClick" #(js/alert "Clicked!")}]
We use the same props serialization logic as Reagent to make migrating to HSX as pain-free as possible.
The same as Reagent - use Clojure metadata. Say you want to pass a React key to a component:
(defn component-with-seq []
[:ol {:className "bg-slate-500"}
(for [item items]
^{:key (str "item-" (:id item))}
[item-component item])])
id
and className
short-hands?The same as Reagent + Hiccup:
[:div#foo ...] ;; => [:div {:id "foo"}]
[:div.foo.bar ...] ;; => [:div {:className "foo bar"}]
Class based components (the ones with lifecycle methods) have been out of style for almost a decade with React.
If you wish to adopt HSX you will need to migrate Reagent class components to function components. Generally this means rewriting the component to use hooks.
However, error boundaries are the one place in the React ecosystem where class components may be required. We suggest using a wrapping library like react-error-boundary instead.
If you still require class-based components, you can always extend js/React.Component.prototype
yourself. See this gist for an example.
componentDidUpdate
logic)?By default, yes, HSX components are wrapped in a react/memo call with an appropriate arePropsEqual?
predicate for ClojureScript data structures.
Note: unlike Reagent, memoization is a performance optimization. Please refer to the official React documentation for more information.
If you'd like to disable memoization by default globally, you can:
{...
:builds
{:app
{:target :browser
:modules {:app {:entries [your.app]}}
:closure-defines {io.factorhouse.hsx.core/USE_MEMO false}
}}}
If you'd like to disable/enable memoization per-component, you can supply a :memo?
key as metadata to the component vector:
[:div
^{:memo? false}
[my-hsx-comp arg1 arg2]]
If you want to use a custom are-props-equal?
predicate for memoization, you can also use component metadata:
;; This custom predicate treats the previous and next state as equal if the value of `:foo` has not changed.
(defn custom-are-props-equal-pred
[[prev-arg1 _prev-arg2] [next-arg1 _next-arg2]]
(= (:foo prev-arg1) (:foo next-arg1)))
[:div
^{:memo? true :memo/predicate custom-are-props-equal-pred}
[my-hsx-comp arg1 arg2]]
The same as Reagent. Denoted by :<>
(defn list-of-things []
[:<>
[:div "First thing"]
[:div "Second thing"]])
The same as Reagent. Denoted by :>
;; (:require ["react-select" :as Select])
(defn dropdown-example [options]
[:> Select {:on-change #(js/alert "Data changed")
:options options}])
Yes, pass a JS Object instead:
;; (:require ["react-select" :as Select])
(defn dropdown-example [options]
[:> Select #js {"onChange" #(js/alert "Data changed")
"options" options}])
Use hsx/reactify-component
. If we use react-error-boundary as an example:
;; (:require ["react-error-boundary" :refer [ErrorBoundary]]
;; [io.factorhouse.hsx.core :as hsx])
(defn fallback-renderer
[{:keys [error]}]
[:div (str "Something went wrong: " error)])
(defn with-error-boundary
[comp]
[:> ErrorBoundary {:fallbackRender (hsx/reactify-component fallback-renderer)}
(hsx/create-element comp)])
(defn my-ui []
[with-error-boundary
[:div "This is my application..."]])
Copyright © 2025 Factor House Pty Ltd.
Distributed under the Apache-2.0 License, the same as Apache Kafka.
Can you improve this documentation? These fine people already did:
Thomas Crowley, Derek Troy-West & Prabhjot SinghEdit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close