{"id":15873907,"url":"https://github.com/wavejumper/reax","last_synced_at":"2026-02-27T22:37:39.492Z","repository":{"id":36074430,"uuid":"221204970","full_name":"wavejumper/reax","owner":"wavejumper","description":"Event driven RPC between a Swift backend and a Clojurescript front end.","archived":false,"fork":false,"pushed_at":"2023-01-05T00:58:11.000Z","size":1917,"stargazers_count":10,"open_issues_count":19,"forks_count":0,"subscribers_count":3,"default_branch":"master","last_synced_at":"2026-02-26T06:14:38.004Z","etag":null,"topics":["clojurescript","native-modules","react-native","swift"],"latest_commit_sha":null,"homepage":null,"language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/wavejumper.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2019-11-12T11:45:57.000Z","updated_at":"2023-11-26T13:37:21.000Z","dependencies_parsed_at":"2023-01-16T13:01:26.329Z","dependency_job_id":null,"html_url":"https://github.com/wavejumper/reax","commit_stats":null,"previous_names":[],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/wavejumper/reax","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wavejumper%2Freax","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wavejumper%2Freax/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wavejumper%2Freax/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wavejumper%2Freax/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/wavejumper","download_url":"https://codeload.github.com/wavejumper/reax/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wavejumper%2Freax/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29917816,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-27T19:37:42.220Z","status":"ssl_error","status_checked_at":"2026-02-27T19:37:41.463Z","response_time":57,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["clojurescript","native-modules","react-native","swift"],"created_at":"2024-10-06T01:08:18.618Z","updated_at":"2026-02-27T22:37:39.464Z","avatar_url":"https://github.com/wavejumper.png","language":"Clojure","readme":"# Reax\n\nEvent driven RPC between a Swift backend and a Clojurescript front end. Influenced by the Xi-editor.\n\n**Note:** API is subject to change as I experiment using reax within my own application.\n\n# Prior reading\n\nHere are some resources to help familiarise yourself with Native Modules for React Native, and the Xi-editor architecture:\n\n* [Native Modules](https://facebook.github.io/react-native/docs/native-modules-ios)\n* [Swift in React Native - The Ultimate Guide Part 1: Modules](https://teabreak.e-spres-oh.com/swift-in-react-native-the-ultimate-guide-part-1-modules-9bb8d054db03)\n* [Xi editor](https://xi-editor.io/)\n\n# Design goals\n\nFollowing Xi's [design decisions](https://github.com/xi-editor/xi-editor#design-decisions):\n\n* **Separation into front-end and back-end modules**\n* **Native UI** \n* **Asynchronous operations** \n* **Functional core, imperative shell** \n* **Programming in data** \n\nThe front end renders a UI derived from the events (facts) it has received. The front end should never block, and is modelled to be eventually consistent.\n\n* This design plays to Clojure's strengths greatly, where data comes first!\n* This design makes the limiting \"single-threaded event loop\" model of the JS runtime a bit easier to accept.\n* This architecture leverages a reasonably expressive back end language (Swift) for its \"imperative shell\"\n\n# Getting started\n\n## Requirements\n\nThis project assumes you have a React Native project targetting `ios` and a tool for building Clojurescript assets (eg [shadow-cljs](https://github.com/thheller/shadow-cljs)).\n\nYour project needs to be able to support native modules. If you are using Expo, you will need to [eject to ExpoKit](https://docs.expo.io/versions/latest/expokit/eject/).\n\nPlease check this [example](https://github.com/wavejumper/reax/tree/master/examples/synth) project for reference.\n\nReax ships with no transitive dependencies. The front end optionally depends on [integrant](https://github.com/weavejester/integrant), though you can use any library for managing app state.\n\n### Clojurescript\n\nAdd this dependency to your projects `deps.edn` file:\n\n```clojure\nwavejumper/reax {:mvn/version \"1.2.0\"}\n```\n\n### Swift\n\nAdd this dependency to your projects `ios/Podfile` file:\n\n```ruby\npod 'RNReax', :git =\u003e 'https://github.com/wavejumper/RNReax', :tag =\u003e '1.2.0'\n```\n\n# Using the library\n\n## Events\n\nA Reax event looks like this: \n\n```clojure\n[\"setFrequency\" {:frequency 400}]\n```\n\nA Reax event can be dispatched from either Clojurescript or Swift, and are considered asynchronous.\n\nGenerally a dispatch event will originate from the front end (eg via user interaction) and the Reax Swift module will asynchronously perform the operation.\n\nIt is up to the end-user to define the semantics of the operation (eg if the event is idempotent). Reax simply provides the glue.\n\nDuring transport, events are represented as JSON (conveniently, thanks to the [codable](https://developer.apple.com/documentation/swift/codable) protocols in Swift, and `js/JSON` in JS). Ideally, a [transit-json](https://github.com/cognitect/transit-format) codable impl would be nicer for richer types.\n\n## Event router\n\nThe Swift type system defines the schema of available events and valid arguments an event has:\n\nAnything `Decodable` is a valid event id, so long as it also implements the `ReaxRouter` protocol. An event id is generally represented as an enum encoded as a string.\n\n```swift\nenum SynthRouter: String, Codable, ReaxRouter {\n  typealias Context = SynthContext\n  typealias Result = SynthResult\n\n  case setFrequency\n  case startSynth\n  case stopSynth\n\n  func routeEvent() -\u003e ((_ ctx: Context, _ from: Data) -\u003e Either\u003cResult, ReaxError\u003e) {\n    switch self {\n    case .setFrequency:\n      return eventHandler(SetFrequncyHandler.self)\n    case .stopSynth:\n      return eventHandler(StopSynthHandler.self)\n    case .startSynth:\n      return eventHandler(StartSynthHandler.self)\n    }\n  }\n}\n```\n\nIn this example `setFrequency` maps to the `SetFrequencyHandler` (defined below). \n\n`eventHandler` is a helper fn that returns a closure that decodes, and invokes the event handler. This fn also neatly handles any deserialization errors.\n\nThe router is succinct for most use cases. The router should only care about matching event id's to event handlers.\n\n## Event handlers\n\nEvent handlers are represented with the `ReaxHandler` protocol. Similar to event ids, anything that implements the `Decodable` protocol is a valid handler. A handler is generally represented as a struct.\n\n```swift\nstruct SetFrequncyHandler: Codable, ReaxHandler {\n  typealias Context = SynthContext\n  typealias Result = SynthResult\n\n  var frequency: Double\n\n  func invoke(ctx: Context) -\u003e Either\u003cResult, ReaxError\u003e {\n    ctx.oscillator.frequency = frequency\n    return Either.left(ctx.status())\n  }\n}\n```\n\n## Context and results\n\nThese are two `typealias` arguments both `ReaxRouter` and `ReaxHandler` must provide to allow for customised context and results.\n\n### Context\n\nThe `Context` typealias allows the end user to define required components  (eg, a database pool or some custom state) that will get passed to the handler's `invoke` method.\n\n`Context` can be any data structure implementing the `ReaxContext` protocol. They are generally structs.\n\n```swift\nstruct SynthContext: ReaxContext {\n  var oscillator: AKOscillator\n  \n  func state() -\u003e ReaxContextState {\n    return ReaxContextState.started\n  }\n}\n```\n\n### Result\n\nThe `Result` typealias is the value the Reax module sends to the front end. It can be represented as anything `Encodable`, and is generally a struct, or enum.\n\n```swift \nstruct SynthResult: Codable {\n  var started: Bool\n}\n```\n\n## Reax errors\n\nIn the case of handling errors, the `ReaxError` enum is returned to the user.\n\n## ReaxEventEmitter\n\nFinally, the `ReaxEventEmitter` class is what the front end interacts with. This class is a subclass of `RCTEventEmitter`. \n\nA skeloton Reax class looks like this:\n\n```swift \n@objc(Synth)\nclass Synth: ReaxEventEmitter {\n  var ctx = SynthContext(oscillator: AKOscillator())\n  \n  // This shouldn't have to be provided by impls, seems like there is an odd bug\n  // with debug builds of React Native...\n  override func supportedEvents() -\u003e [String]! {\n    return [self.errorType(), self.resultType()]\n  }\n  \n  @objc\n  func start() {\n    AudioKit.output = self.ctx.oscillator\n    try! AudioKit.start()\n  }\n\n  @objc\n  func stop() {\n    try! AudioKit.stop()\n  }\n  \n  @objc(dispatch:args:)\n  func dispatch(id: NSString, args: NSString) {\n    self.invoke(SynthRouter.self, ctx: self.ctx, id: id as String, args: args as String)\n  }\n}\n```\n\n### dispatch\n\nThis method gets called when the front end sends an event to the module.\n\n### Other helper methods\n\nThe `ReaxEventEmitter` class contains one useful method, `channelFactory` for constructing 'channels' out of the modules concrete `Result` type.\n\nThis can be used for dispatching results to the front end asynchronously, eg some event triggered by [Key-Value Observing](https://nshipster.com/key-value-observing/)\n\n### Boilerplate\n\nA `\u003cModuleName.m\u003e` file will also have to be created, exposing the module to the front end:\n\n```objc\n#import \"React/RCTBridgeModule.h\"\n#import \"React/RCTEventEmitter.h\"\n\n@interface RCT_EXTERN_MODULE(Synth, RCTEventEmitter)\n\nRCT_EXTERN_METHOD(start)\nRCT_EXTERN_METHOD(stop)\nRCT_EXTERN_METHOD(dispatch:(NSString *)id args:(NSString *)args)\n\n@end\n```\n\n## Reax lifecycle\n\nAll `ReaxEventEmitter` classes have to implement `start` and `stop` methods, which will be invoked from the front end when the user initializes the Reax module.\n\nThese two methods are how you manage the lifecycle of stateful Reax modules, eg setting up `Context`.\n\nThis pattern means that both configuration and lifecycle management should come from a centralized location: your front-end (eg, via [integrant](https://github.com/weavejester/integrant)).\n\nIf you need more custom dependency injection, you can always implement the [RCTBridgeDelegate protocol](https://facebook.github.io/react-native/docs/native-modules-ios#dependency-injection)\n\n## Clojurescript \n\n### Integrant\n\nThe `reax.integrant` namespace provides [integrant](https://github.com/weavejester/integrant) integration via the `:reax/module` key.\n\nWithin your integrant config, you can define a Reax module like so:\n\n```clojure\n(ns example\n  (:require [integrant.core :as ig]\n            [example.link :as link]\n            [reax.integrant])) ;; \u003c-- require for :reax/module key\n\n(defmethod ig/init-key :app/db [_ init-value]\n  (atom init-value))\n\n(defn synth-result-handler\n  [db event]\n  (assoc db :synth/state event))\n\n(defn synth-error-handler\n  [db event]\n  (js/console.warn \"Synth error\" (pr-str event))\n  db)\n\n(defn wrap-db [db handler]\n  (fn [event]\n    (swap! db handler event)))\n\n(defmethod ig/init-key :app/db-handler [_ {:keys [db handler]}]\n  (wrap-db db handler))\n\n(defn config []\n  {;; The atom housing our application state.\n   :app/db                                 {} ;; \u003c--- initial app state\n\n   ;; The result handler for our synth\n   [:app/db-handler :synth/result-handler] {:db      (ig/ref :app/db)\n                                            :handler synth-result-handler}\n\n   ;; The error handler for our synth\n   [:app/db-handler :synth/error-handler]  {:db      (ig/ref :app/db)\n                                            :handler synth-error-handler}\n\n   ;; A reax module for our synth\n   [:reax/module :reax/synth]              {:class-name     \"Synth\" ;; \u003c-- the objc class of our reax module\n                                            :result-handler (ig/ref :synth/result-handler)\n                                            :error-handler  (ig/ref :synth/error-handler)}})\n\n```\n\n`:reax/module` accepts three keys:\n\n* `:class-name`: the name of the Reax objc class\n* `:result-handler`: a function that handles all Reax results\n* `:error-handler`: a function that handles all Reax errors\n\nThis is how you send events to your Reax module:\n\n```clojure\n(require '[reax.core :as reax])\n\n(reax/dispatch module \"setFrequency\" {:frequency 400})\n```\n\nAll serialization will be taken care of!\n\n# Notes / considerations\n\n* All Reax modules are singletons, because all Native Modules are singletons. \n\n# TODOs / Design questions\n\n## Allow for custom arguments to be passed into the start method\n\nFor neater dependency injection, it would be nice if the `start` method had a way to pass in generic args to the component...\n\n## Javascript front end\n\nThere is nothing in this codebase that ties it to just Clojurescript. Look to the [:npm-module](https://shadow-cljs.github.io/docs/UsersGuide.html#target-npm-module) target in shadow-cljs and offer NPM package.\n\n## Unit tests\n\nThey would be good :)\n\n## Remove the boilerplate of the Objective-C declaration\n\nIf there is an easy way to template/automatically define the boilerplate-y `\u003cModuleName.m\u003e`  `RCT_EXTERN_MODULE`  code that would be nice.\n\n## Remove the idea of the result type\n\nThe result type was added as a way to avoid having the `invoke` operation return meaningless voids everywhere. \n\nTo better represent the async nature of Reax, and if all mutations are generally something side-effectful, perhaps something like [bow effects](https://bow-swift.io/docs/effects/overview/) would be a good idea for richer types.\n\n## Define a better way for custom user errors\n\nThe `ReaxError` enum isn't really extensible, and without [higher kinded types](https://www.stephanboyer.com/post/115/higher-rank-and-higher-kinded-types), I'm not really sure of the best way to allow for better extensibility.\n\n## RCTConvert\n\nJSON transport makes for a convenient, and type safe API thanks to codable. [RCTConvert](https://github.com/facebook/react-native/blob/master/React/Base/RCTConvert.h) allows for helper functions all accept a JSON value as input and map it to a native Objective-C type or class. \n\nInvestigate this as an alternative for performance reasons.\n\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwavejumper%2Freax","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fwavejumper%2Freax","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwavejumper%2Freax/lists"}