ClojureRNProject
                                
                                 ClojureRNProject copied to clipboard
                                
                                    ClojureRNProject copied to clipboard
                            
                            
                            
                        Simple React Native application with ClojureScript, re-frame and react navigation v5
hackmd: https://hackmd.io/@byc70E6fQy67hPMN0WM9_A/rJilnJxE8
Confidence and Joy: React Native Development with ClojureScript and re-frame
Clojure: https://clojure.org/guides/getting_started
Code editor: IntelliJ IDEA Community https://www.jetbrains.com/idea/download/ with Cursive plugin https://cursive-ide.com/
shadow-cljs: http://shadow-cljs.org/ re-frame-steroid: https://github.com/flexsurfer/re-frame-steroid rn-shadow-steroid: https://github.com/flexsurfer/rn-shadow-steroid
PROJECT SOURCES: https://github.com/flexsurfer/ClojureRNProject
1. Create a new React Native Project or open existing one
react-native init ClojureRNProject
cd ClojureRNProject
Open project in IDE

Edit App.js
import React from 'react';
import {
  SafeAreaView,
  View,
  Text,
} from 'react-native';
const App: () => React$Node = () => {
  return (
    <>
      <SafeAreaView>
        <View>
        <Text>Hello CLojure!</Text>
        </View>
      </SafeAreaView>
    </>
  );
};
export default App;
Run the app
Terminal 1: yarn start
Terminal 2: yarn ios

OK, now we have RN project and we want to run the same app but with clojure
2. Add shadow-cljs
yarn add shadow-cljs
If you already have it, make sure you are using the latest version
Create shadow-cljs.edn
{:source-paths ["src"]
 :dependencies [[reagent "0.10.0"]
                [re-frame "0.12.0"]
                [re-frame-steroid "0.1.1"]
                [rn-shadow-steroid "0.2.1"]
                [re-frisk-remote "1.3.3"]]
 :builds       {:dev
                {:target     :react-native
                 :init-fn    clojurernproject.core/init
                 :output-dir "app"
                 :compiler-options {:closure-defines
                                    {"re_frame.trace.trace_enabled_QMARK_" true}}
                 :devtools   {:after-load steroid.rn.core/reload
                              :build-notify steroid.rn.core/build-notify
                              :preloads [re-frisk-remote.preload]}}}}
Next, 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
3. Create cljs project
create deps.edn file
{:deps  {org.clojure/clojure       {:mvn/version "1.10.0"}
         org.clojure/clojurescript {:mvn/version "1.10.339"}
         reagent                   {:mvn/version "0.10.0"}
         re-frame                  {:mvn/version "0.12.0"}
         re-frame-steroid          {:mvn/version "0.1.1"}
         rn-shadow-steroid         {:mvn/version "0.2.1"}}
 :paths ["src"]}
Right click on the file and Add as Clojure Deps Project

Optional turn off a spelling
Indellij IDEA -> Preferences

create src folder and clojurernproject package with core.cljs file

core.cljs
(ns clojurernproject.core
  (:require [steroid.rn.core :as rn]))
(defn root-comp []
  [rn/safe-area-view
   [rn/view
    [rn/text "Hello CLojure! from CLJS"]]])
(defn init []
  (rn/register-reload-comp "ClojureRNProject" root-comp))
index.js
import "./app/index.js";
Terminal 3: shadow-cljs watch dev
Reload the app
Disable Fast Refresh
Cmnd+D

Now try to change the code, you should see it reloaded by shadow-cljs in the app
now you have clojurescript RN app configured with hot reload
4. App state with re-frame
To update app state, we need to use events, let's create events.cljs and register our first events
events.cljs
(ns clojurernproject.events
  (:require [steroid.fx :as fx]))
(fx/defn
  init-app-db
  {:events [:init-app-db]}
  [_]
  {:db {:counter 0}})
(fx/defn
  update-counter
  {:events [:update-counter]}
  [{:keys [db]}]
  {:db (update db :counter inc)})
Set cursor on fx/defn and press option+return

Move selection to Resolve .. as... and press return then select defn
To update a view when the state is changed, we need to use subscriptions, let's create subs.cljs and register subscriptions.
subs.cljs
(ns clojurernproject.subs
  (:require [steroid.subs :as subs]))
(subs/reg-root-subs #{:counter})
Now we can update our view
core.cljs
(ns clojurernproject.core
  (:require [steroid.rn.core :as rn]
            [steroid.views :as views]
            [re-frame.core :as re-frame]
            clojurernproject.events
            clojurernproject.subs))
(views/defview root-comp []
  (views/letsubs [counter [:counter]]
    [rn/safe-area-view {:style {:flex 1}}
     [rn/view {:style {:align-items :center :justify-content :center :flex 1}}
      [rn/text (str "Counter: " counter)]
      [rn/touchable-opacity {:on-press #(re-frame/dispatch [:update-counter])}
       [rn/view {:style {:background-color :gray :padding 5}}
        [rn/text "Update counter"]]]]]))
(defn init []
  (re-frame/dispatch [:init-app-db])
  (rn/register-reload-comp "ClojureRNProject" root-comp))
Resolve defview as defn and letsubs as let same way how we did it for fx/defn
you can press "Update counter" button, and then change your code, and you can see app updated, but app state remained the same

now you have clojurescript RN app configured with hot reload and re-frame state
There are three major rules when working with re-frame
- views are pure and dumb, just render UI with data from subscriptions and dispatch events
Bad:
(views/defview comp []
  (views/letsubs [counter [:counter]
                  delta [:delta]]
    [rn/text (str "Counter: " (+ counter delta))]
    [rn/touchable-opacity 
     {:on-press #(re-frame/dispatch 
                  [:update-counter (if (> delta 12) 
                                     counter 
                                     delta)])}]))
Good:
(views/defview comp []
  (views/letsubs [counter-with-delta [:counter-with-delta]]
    [rn/text (str "Counter: " counter-with-delta)]
    [rn/touchable-opacity 
     {:on-press #(re-frame/dispatch [:update-counter])}]))
we have a separate subscription and event will get all data from the state
- Only root keys should be subscribed on app-db
Bad:
(re-frame/reg-sub :counter (fn [db] (get db :counter)))
(re-frame/reg-sub :delta (fn [db] (get db :delta)))
(re-frame/reg-sub :counter-with-delta (fn [db] (+ (get db :counter) (get db :delta)))
Good:
(subs/reg-root-subs #{:counter :delta})
(re-frame/reg-sub
 :counter-with-delta
 :<- [:counter]
 :<- [:delta]
 (fn [[counter delta]]
   (+ counter delta)))
- Events must be pure and do all computations
Bad:
(fx/defn
  update-counter
  {:events [:update-counter]}
  [{:keys [db]}]
  (do-something)
  {:db (update db :counter inc)})
Good:
(re-frame/reg-fx
  :do-something
  (fn []
    (do-something)))
(fx/defn
  update-counter
  {:events [:update-counter]}
  [{:keys [db]}]
  {:db (update db :counter inc)
   :do-something nil})
6. Devtools
let's run re-frisk debugging tool and see what's exactly happening in the app
Terminal 4: shadow-cljs run re-frisk-remote.core/start
and open http://localhost:4567

You can see all that is happening with the app: events, app-db (state) and subscriptions
6. Tests
Add test folder and configure test build in the project
{:source-paths ["src" "test"]
 :dependencies [[...]]
 :builds       {:dev
                {...}
                :test
                {:target    :node-test
                 :output-to "out/node-tests.js"
                 :autorun   true}}}
Let's add some tests
events/counter_test.cljs
(ns events.counter-test
  (:require [cljs.test :refer (deftest is)]
            [clojurernproject.events :as events]))
(deftest events-counter-test
  (is (= (events/update-counter {:db {:counter 0}})
         {:db {:counter 1}})))
And run tests
Terminal 3: shadow-cljs compile test

7. Navigation
React Navigation 5
Terminal 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
Terminal 2: cd ios; pod install; cd ..
Terminal 2: yarn ios
core.cljs
(ns clojurernproject.core
  (:require [steroid.rn.core :as rn]
            [re-frame.core :as re-frame]
            [steroid.rn.navigation.core :as rnn]
            [steroid.rn.navigation.stack :as stack]
            [steroid.rn.navigation.bottom-tabs :as bottom-tabs]
            [clojurernproject.views :as screens]
            [steroid.rn.navigation.safe-area :as safe-area]
            steroid.rn.navigation.events
            clojurernproject.events
            clojurernproject.subs))
(defn main-screens []
  [bottom-tabs/bottom-tab
   [{:name      :home
     :component screens/home-screen}
    {:name      :basic
     :component screens/basic-screen}
    {:name      :ui
     :component screens/ui-screen}
    {:name      :list
     :component screens/list-screen}
    {:name      :storage
     :component screens/storage-screen}]])
(defn root-stack []
  [safe-area/safe-area-provider
   [(rnn/create-navigation-container-reload
     {:on-ready #(re-frame/dispatch [:init-app-db])}
     [stack/stack {:mode :modal :header-mode :none}
      [{:name      :main
        :component main-screens}
       {:name      :modal
        :component screens/modal-screen}]])]])
(defn init []
  (rn/register-comp "ClojureRNProject" root-stack))
For 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
After we've required steroid.rn.navigation.events ns we can dispatch :navigate-to and :navigate-back events for navigation between screens
Try to open modal screen and change the code you will see that navigation state isn't changed, the modal screen will be still opened

КОНЕЦ