Awesome
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
КОНЕЦ