RFX is a modern, API-compatible drop-in replacement for re-frame, designed for use with React 18+ and no dependency on Reagent. Its API is based on hooks.
It integrates seamlessly with vanilla React, popular wrapper libraries like uix or even Reagent.
See also: HSX - a ClojureScript library for writing React components using Hiccup syntax.
React 19 introduced significant updates to React's rendering pipeline, which are incompatible with Reagent.
At Factor House, our products require modern React API features without the technical debt of Reagent.
If you want to read more about the engineering challenge of moving a 120k LOC Reagent codebase to React 19 read this blog post.
re-frame.core
API (for migrating codebases);; deps.edn
{:deps {io.factorhouse/re-frame-bridge {:mvn/version "0.1.13"}}}
The io.factorhouse/re-frame-bridge
library is a drop-in replacement for re-frame allowing you use RFX via a re-frame.core
shim namespace.
This library is intended to be used by existing codebases who are seeking to migrate off Reagent/re-frame.
As this is a compatibility layer, advanced features of RFX (such as React Contexts and hooks) cannot be used as ergonomically.
Check out the re-frame-bridge-todo-mvc example for reference.
io.factorhouse.rfx.core
API (recommended);; deps.edn
{:deps {io.factorhouse/rfx {:mvn/version "0.1.13"}}}
Consumers of RFX interact with the API through the io.factorhouse.rfx.core
namespace.
Check out the rfx-todo-mvc example for reference.
RFX uses React Context as the means for components to access the RFX instance.
Building on top of React contexts offers several advantages compared to re-frame's global state approach:
The io.factorhouse.rfx.core/RFXContextProvider
provides an RFX instance to its children.
Wrap your root component with an RFXContextProvider
to get started:
;; (:require [io.factorhouse.rfx.core :as rfx])
;; Option 1: Use global application state (like re-frame)
;; Wrapping your root component with no explicit RFXContextProvider uses the global RFX instance:
[my-root-component] ;; Equivalent to [:> rfx/RFXContextProvider #js {} [my-root-component]]
;; Option 2: Initialize your own scoped context
(defonce custom-rfx-ctx (rfx/init {:initial-value {:foo :bar}}))
[:> rfx/RFXContextProvider #js {"value" custom-rfx-ctx}
[my-root-component]]
Hook | Description |
---|---|
use-sub | Subscribe to data from the state store |
use-dispatch | Dispatch events to trigger state changes |
Both of these can be used within components like so:
;; (require '[io.factorhouse.rfx.core :as rfx])
(rfx/reg-sub :counter (fn [db _] (:counter db)))
(rfx/reg-event-db :counter/increment (fn [db _] (update db :counter inc)))
(defn my-root-component []
(let [dispatch (rfx/use-dispatch)
counter (rfx/use-sub [:counter])]
[:div {:on-click #(dispatch [:counter/increment])}
"The value of counter is " counter]))
The parent RFXContextProvider
determines:
use-sub
will subscribe todispatch
will send events toThis context isolation allows components to be developed and tested independently, greatly simplifying integration with tools like StorybookJS.
(defmethod storybook/story "Kpow/Sampler/KJQFilter" [_]
(let [{:keys [dispatch] :as ctx} (rfx/init {})]
{:component [:> rfx/RFXContextProvider #js {"value" ctx} [kjq/editor "kjq-filter-label"]]
:stories {:Filter {}
:ValidInput {:play (fn [_]
(dispatch [:input/context :kjq "foo"])
(dispatch [:input/context :kjq "foo"]))}}}))
In this example, each story operates within its own isolated context, allowing components to be tested independently. This approach makes it easier to develop, test, and iterate on individual components when using component-driven development tools.
So far you have only seen how to interface with RFX from within React components (via React Contexts and Hooks).
However, you'll often have systems external to React that need to integrate with RFX:
External systems need to specify which RFX instance they would like to communicate with via an extra argument when dispatching:
(defonce rfx-context (rfx/init {}))
;; Some imaginary ws-instance
(.on ws-instance "message" #(rfx/dispatch rfx-context [:ws/message %]))
This adds little overhead, as it's typical to initialize all your services within an init
function that has scope to your application's RFX context:
(defn init []
(let [rfx (rfx/init {})]
(init-ws-conn! rfx)
(init-reitit-router! rfx)
(render-my-react rfx)))
You can get the current value of a subscription outside of a React context by calling io.factorhouse.rfx.core/snapshot-sub
:
(defn codemirror-autocomplete-suggestions
[rfx]
(let [database-completions (rfx/snapshot-sub rfx [:ksql/database-completions])]
;; Logic to wire up codemirror6 completions based on re-frame data goes here
))
This might be one of RFX's major selling points! Accessing subscriptions outside of React with re-frame has always been cumbersome and somewhat hacky.
You can access the current value of the application db by calling io.factorhouse.rfx.core/snapshot
.
Note: Both snapshot
and snapshot-sub
are not 'reactive' - they will not cause a re-render of a component when values change. These functions are intended to be used outside a React context.
Calling io.factorhouse.rfx.core/init
returns a new RFX instance. So far we have only seen how to use this instance, but not how to configure it.
rfx/init
accepts the following keys:
Key | Required | Description |
---|---|---|
:queue | ❌ | The event queue used to process messages. Default queue is the same as re-frame's (uses goog.async.nextTick to process events) |
:error-handler | ❌ | Error handler (default ErrorHandler is the same as re-frame's - something that logs and continues) |
:store | ❌ | The store used to house your application's state. Default store is backed by a Clojure atom. |
:initial-value | ❌ | The initial value of the store. Default is {} . |
:registry | ❌ | The event+subscription registry the RFX instance will use. Defaults to the global registry. |
Even the re-frame.core/subscribe
function returns a subscription hook wrapped in a Clojure delay.
This means you can use RFX from any React wrapper (like HSX or Uix) or even plain JavaScript.
Note: All the caveats of React hooks also apply to RFX subscriptions!
Reagent users will need to wrap components in the :f>
function component shorthand:
(defn rfx-interop []
(let [val @(re-frame.core/subscribe [:some-value])]
[:div "The result is " val]))
(defn my-reagent-comp []
[:f> rfx-interop])
^:flush-dom
annotations^:flush-dom
metadata is not supported like in re-frame.
We highly recommend reading the excellent official re-frame docs to understand the architecture that RFX builds upon.
Distributed under the Apache 2.0 License.
Copyright (c) Factor House
Can you improve this documentation? These fine people already did:
Thomas Crowley & Derek Troy-WestEdit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close