{"id":15648010,"url":"https://github.com/flexsurfer/clojurernproject","last_synced_at":"2025-08-20T20:32:50.107Z","repository":{"id":38848277,"uuid":"242795305","full_name":"flexsurfer/ClojureRNProject","owner":"flexsurfer","description":"Simple React Native application with ClojureScript, re-frame and react navigation v5","archived":false,"fork":false,"pushed_at":"2025-02-13T12:21:25.000Z","size":473,"stargazers_count":50,"open_issues_count":4,"forks_count":8,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-04-07T20:50:07.135Z","etag":null,"topics":["clojure","clojurescript","re-frame","react-native","react-navigation","react-navigation-v5","shadow-cljs","shadow-cljs-app"],"latest_commit_sha":null,"homepage":"","language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/flexsurfer.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-02-24T17:11:35.000Z","updated_at":"2024-12-11T07:10:50.000Z","dependencies_parsed_at":"2024-10-22T23:51:33.590Z","dependency_job_id":"b68c0bc7-eeb0-4cac-924f-33257ef8c361","html_url":"https://github.com/flexsurfer/ClojureRNProject","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/flexsurfer/ClojureRNProject","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/flexsurfer%2FClojureRNProject","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/flexsurfer%2FClojureRNProject/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/flexsurfer%2FClojureRNProject/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/flexsurfer%2FClojureRNProject/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/flexsurfer","download_url":"https://codeload.github.com/flexsurfer/ClojureRNProject/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/flexsurfer%2FClojureRNProject/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":271378680,"owners_count":24749192,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-08-20T02:00:09.606Z","response_time":69,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"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":["clojure","clojurescript","re-frame","react-native","react-navigation","react-navigation-v5","shadow-cljs","shadow-cljs-app"],"created_at":"2024-10-03T12:22:41.396Z","updated_at":"2025-08-20T20:32:49.801Z","avatar_url":"https://github.com/flexsurfer.png","language":"Clojure","funding_links":[],"categories":[],"sub_categories":[],"readme":"hackmd: https://hackmd.io/@byc70E6fQy67hPMN0WM9_A/rJilnJxE8\n\n# Confidence and Joy: React Native Development with ClojureScript and re-frame\n\nClojure: https://clojure.org/guides/getting_started\n\nCode editor: IntelliJ IDEA Community https://www.jetbrains.com/idea/download/\nwith Cursive plugin https://cursive-ide.com/\n\nshadow-cljs: http://shadow-cljs.org/\nre-frame-steroid: https://github.com/flexsurfer/re-frame-steroid\nrn-shadow-steroid: https://github.com/flexsurfer/rn-shadow-steroid\n\nPROJECT SOURCES: https://github.com/flexsurfer/ClojureRNProject\n\n### 1. Create a new React Native Project or open existing one\n\n`react-native init ClojureRNProject`\n\n`cd ClojureRNProject`\n\nOpen project in IDE\n\n![](https://i.imgur.com/GFLzmOi.png)\n\nEdit `App.js`\n\n```jsx=\nimport React from 'react';\nimport {\n  SafeAreaView,\n  View,\n  Text,\n} from 'react-native';\n\nconst App: () =\u003e React$Node = () =\u003e {\n  return (\n    \u003c\u003e\n      \u003cSafeAreaView\u003e\n        \u003cView\u003e\n        \u003cText\u003eHello CLojure!\u003c/Text\u003e\n        \u003c/View\u003e\n      \u003c/SafeAreaView\u003e\n    \u003c/\u003e\n  );\n};\n\nexport default App;\n```\n\nRun the app\n\nTerminal 1: `yarn start`\nTerminal 2: `yarn ios`\n\n![](https://i.imgur.com/uO6xvCK.png)\n\n\nOK, now we have RN project and we want to run the same app but with clojure\n\n\n### 2. Add shadow-cljs\n\n`yarn add shadow-cljs`\n\nIf you already have it, make sure you are using the latest version\n\nCreate `shadow-cljs.edn`\n\n```clojure\n{:source-paths [\"src\"]\n\n :dependencies [[reagent \"0.10.0\"]\n                [re-frame \"0.12.0\"]\n                [re-frame-steroid \"0.1.1\"]\n                [rn-shadow-steroid \"0.2.1\"]\n                [re-frisk-remote \"1.3.3\"]]\n\n :builds       {:dev\n                {:target     :react-native\n                 :init-fn    clojurernproject.core/init\n                 :output-dir \"app\"\n                 :compiler-options {:closure-defines\n                                    {\"re_frame.trace.trace_enabled_QMARK_\" true}}\n                 :devtools   {:after-load steroid.rn.core/reload\n                              :build-notify steroid.rn.core/build-notify\n                              :preloads [re-frisk-remote.preload]}}}}\n```\n\nNext, we need to initialize project as Clojure Deps, `deps.edn` will be used only for code inspection in IDE, if you know a better way pls file a PR\n\n### 3. Create cljs project\n\ncreate `deps.edn` file\n\n```clojure\n{:deps  {org.clojure/clojure       {:mvn/version \"1.10.0\"}\n         org.clojure/clojurescript {:mvn/version \"1.10.339\"}\n         reagent                   {:mvn/version \"0.10.0\"}\n         re-frame                  {:mvn/version \"0.12.0\"}\n         re-frame-steroid          {:mvn/version \"0.1.1\"}\n         rn-shadow-steroid         {:mvn/version \"0.2.1\"}}\n :paths [\"src\"]}\n```\n\nRight click on the file and `Add as Clojure Deps Project`\n\n![](https://i.imgur.com/C110quU.png)\n\nOptional turn off a spelling\n\nIndellij IDEA -\u003e Preferences\n\n![](https://i.imgur.com/eqWzrqM.png)\n\ncreate `src` folder and `clojurernproject` package with `core.cljs` file\n\n![](https://i.imgur.com/gDEWfL3.png)\n\n\ncore.cljs\n\n```clojure\n(ns clojurernproject.core\n  (:require [steroid.rn.core :as rn]))\n\n(defn root-comp []\n  [rn/safe-area-view\n   [rn/view\n    [rn/text \"Hello CLojure! from CLJS\"]]])\n\n(defn init []\n  (rn/register-reload-comp \"ClojureRNProject\" root-comp))\n\n```\n\nindex.js\n\n```javascript=\nimport \"./app/index.js\";\n```\n\nTerminal 3: `shadow-cljs watch dev`\n\nReload the app\n\n**Disable Fast Refresh**\n\nCmnd+D\n\n![](https://i.imgur.com/7sOO4Ko.png)\n\nNow try to change the code, you should see it reloaded by shadow-cljs in the app\n\nnow you have clojurescript RN app configured with hot reload\n\n\n### 4. App state with re-frame\n\nTo update app state, we need to use events, let's create `events.cljs` and register our first events\n\nevents.cljs\n```clojure\n(ns clojurernproject.events\n  (:require [steroid.fx :as fx]))\n\n(fx/defn\n  init-app-db\n  {:events [:init-app-db]}\n  [_]\n  {:db {:counter 0}})\n\n(fx/defn\n  update-counter\n  {:events [:update-counter]}\n  [{:keys [db]}]\n  {:db (update db :counter inc)})\n```\n\nSet cursor on `fx/defn` and press `option+return` \n\n![](https://i.imgur.com/4ahMkVJ.png)\n\nMove selection to `Resolve .. as...` and press `return` then select `defn`\n\nTo update a view when the state is changed, we need to use subscriptions, let's create `subs.cljs` and register subscriptions.\n\nsubs.cljs\n```clojure\n(ns clojurernproject.subs\n  (:require [steroid.subs :as subs]))\n\n(subs/reg-root-subs #{:counter})\n```\n\nNow we can update our view\n\ncore.cljs\n```clojure\n(ns clojurernproject.core\n  (:require [steroid.rn.core :as rn]\n            [steroid.views :as views]\n            [re-frame.core :as re-frame]\n            clojurernproject.events\n            clojurernproject.subs))\n\n(views/defview root-comp []\n  (views/letsubs [counter [:counter]]\n    [rn/safe-area-view {:style {:flex 1}}\n     [rn/view {:style {:align-items :center :justify-content :center :flex 1}}\n      [rn/text (str \"Counter: \" counter)]\n      [rn/touchable-opacity {:on-press #(re-frame/dispatch [:update-counter])}\n       [rn/view {:style {:background-color :gray :padding 5}}\n        [rn/text \"Update counter\"]]]]]))\n\n(defn init []\n  (re-frame/dispatch [:init-app-db])\n  (rn/register-reload-comp \"ClojureRNProject\" root-comp))\n```\n\nResolve `defview` as `defn` and `letsubs` as `let` same way how we did it for `fx/defn`\n\nyou can press \"Update counter\" button, and then change your code, and you can see app updated, but app state remained the same\n\n![](https://i.imgur.com/T5wfvnX.png)\n\nnow you have clojurescript RN app configured with hot reload and re-frame state\n\nThere are three major rules when working with re-frame\n1) views are pure and dumb, just render UI with data from subscriptions and dispatch events\n\nBad: \n```clojure\n(views/defview comp []\n  (views/letsubs [counter [:counter]\n                  delta [:delta]]\n    [rn/text (str \"Counter: \" (+ counter delta))]\n    [rn/touchable-opacity \n     {:on-press #(re-frame/dispatch \n                  [:update-counter (if (\u003e delta 12) \n                                     counter \n                                     delta)])}]))\n```\n\nGood: \n```clojure\n(views/defview comp []\n  (views/letsubs [counter-with-delta [:counter-with-delta]]\n    [rn/text (str \"Counter: \" counter-with-delta)]\n    [rn/touchable-opacity \n     {:on-press #(re-frame/dispatch [:update-counter])}]))\n```\nwe have a separate subscription and event will get all data from the state\n\n2. Only root keys should be subscribed on app-db\n\nBad:\n```clojure\n(re-frame/reg-sub :counter (fn [db] (get db :counter)))\n\n(re-frame/reg-sub :delta (fn [db] (get db :delta)))\n\n(re-frame/reg-sub :counter-with-delta (fn [db] (+ (get db :counter) (get db :delta)))\n```\n\nGood:\n\n```clojure\n(subs/reg-root-subs #{:counter :delta})\n\n(re-frame/reg-sub\n :counter-with-delta\n :\u003c- [:counter]\n :\u003c- [:delta]\n (fn [[counter delta]]\n   (+ counter delta)))\n```\n\n3. Events must be pure and do all computations\n\nBad:\n```clojure\n(fx/defn\n  update-counter\n  {:events [:update-counter]}\n  [{:keys [db]}]\n  (do-something)\n  {:db (update db :counter inc)})\n```\n\nGood:\n```clojure\n(re-frame/reg-fx\n  :do-something\n  (fn []\n    (do-something)))\n\n(fx/defn\n  update-counter\n  {:events [:update-counter]}\n  [{:keys [db]}]\n  {:db (update db :counter inc)\n   :do-something nil})\n```\n\n### 6. Devtools\n\nlet's run re-frisk debugging tool and see what's exactly happening in the app\n\nTerminal 4: `shadow-cljs run re-frisk-remote.core/start`\n\nand open `http://localhost:4567`\n\n![](https://i.imgur.com/6ty7nbr.png)\n\nYou can see all that is happening with the app: events, app-db (state) and subscriptions\n\n### 6. Tests\n\nAdd test folder and configure test build in the project \n\n```clojure\n{:source-paths [\"src\" \"test\"]\n\n :dependencies [[...]]\n\n :builds       {:dev\n                {...}\n\n                :test\n                {:target    :node-test\n                 :output-to \"out/node-tests.js\"\n                 :autorun   true}}}\n```\n\nLet's add some tests\n\nevents/counter_test.cljs\n\n```clojure\n(ns events.counter-test\n  (:require [cljs.test :refer (deftest is)]\n            [clojurernproject.events :as events]))\n\n(deftest events-counter-test\n  (is (= (events/update-counter {:db {:counter 0}})\n         {:db {:counter 1}})))\n```\n\nAnd run tests\n\nTerminal 3: `shadow-cljs compile test`\n\n![](https://i.imgur.com/28gspBL.png)\n\n### 7. Navigation\n\nReact Navigation 5\n\nTerminal 2: `yarn add @react-navigation/native @react-navigation/stack @react-navigation/bottom-tab react-native-reanimated react-native-gesture-handler react-native-screens react-native-safe-area-context @react-native-community/masked-view`\n\nTerminal 2: `cd ios; pod install; cd ..`\n\nTerminal 2: `yarn ios`\n\ncore.cljs\n```clojure\n(ns clojurernproject.core\n  (:require [steroid.rn.core :as rn]\n            [re-frame.core :as re-frame]\n            [steroid.rn.navigation.core :as rnn]\n            [steroid.rn.navigation.stack :as stack]\n            [steroid.rn.navigation.bottom-tabs :as bottom-tabs]\n            [clojurernproject.views :as screens]\n            [steroid.rn.navigation.safe-area :as safe-area]\n            steroid.rn.navigation.events\n            clojurernproject.events\n            clojurernproject.subs))\n\n(defn main-screens []\n  [bottom-tabs/bottom-tab\n   [{:name      :home\n     :component screens/home-screen}\n    {:name      :basic\n     :component screens/basic-screen}\n    {:name      :ui\n     :component screens/ui-screen}\n    {:name      :list\n     :component screens/list-screen}\n    {:name      :storage\n     :component screens/storage-screen}]])\n\n(defn root-stack []\n  [safe-area/safe-area-provider\n   [(rnn/create-navigation-container-reload\n     {:on-ready #(re-frame/dispatch [:init-app-db])}\n     [stack/stack {:mode :modal :header-mode :none}\n      [{:name      :main\n        :component main-screens}\n       {:name      :modal\n        :component screens/modal-screen}]])]])\n\n(defn init []\n  (rn/register-comp \"ClojureRNProject\" root-stack))\n```\n\nFor hot reload we need to register components differently, we register `root-stack` as regular not reloadable component `rn/register-comp` but we use `rnn/create-navigation-container-reload` for navigation container\n\nAfter we've required `steroid.rn.navigation.events` ns we can dispatch `:navigate-to` and `:navigate-back` events for navigation between screens\n\nTry to open modal screen and change the code you will see that navigation state isn't changed, the modal screen will be still opened\n\n![IMG](https://github.com/flexsurfer/rn-shadow-steroid/raw/master/screencast.gif)\n\nКОНЕЦ","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fflexsurfer%2Fclojurernproject","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fflexsurfer%2Fclojurernproject","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fflexsurfer%2Fclojurernproject/lists"}