-
Notifications
You must be signed in to change notification settings - Fork 1
Navigating between screens
React Native lets us render views but doesn't solve the problem of transitioning between screens.
Navigation concerns include:
- rendering common elements (headers)
- rendering navigation elements (back button)
- keeping track of global history stack
- navigating between screens
- animations when moving between screens
- hiding the keyboard when navigating
- debouncing
- opening / closing the drawer
React Navigation has emerged as the new preferred solution.
- We will use React Navigation as it's intended to be used which means some tradeoffs:
- We won't using the app-db as the primary place to store the navigation state
- 👍 We don't need to ignore the big scary warnings that externalising state will cause performance issues.
- 👎 Feels like losing control of some state and violating the "all state in one location" principle.
- We will navigate without the navigation prop to which requires a little trickery access to the top level navigator
- We will hold off on seantempesta/cljs-react-navigation until we see if the basic interop is enough
- We won't using the app-db as the primary place to store the navigation state
We'll add a few new files for this:
interop.react-navigation
will provide an interop layer including some basic utils
Re-frame integration will be provided by:
mercury-app.cofx.navigator-cofx
mercury-app.events.navigator-events
mercury-app.fx.navigator-fx
The app will configure screens and navigation settings via the mercury-app.nav
ns.
The interop ns massages the raw functionality into an API we can work with.
(ns interop.react-navigation
(:refer-clojure :exclude [pop replace])
(:require [goog.object :as gobj]
[cljs.spec.alpha :as s]
[reagent.core :as r]))
(set! *warn-on-infer* true)
(js/require "react-native-gesture-handler")
(defonce ReactNavigation ^js/ReactNavigation (js/require "react-navigation"))
(defonce createAppContainer (.-createAppContainer ReactNavigation))
(defonce createStackNavigator (.-createStackNavigator ReactNavigation))
(defonce createSwitchNavigator (.-createSwitchNavigator ReactNavigation))
(defonce NavigationActions ^js/ReactNavigation.NavigationActions (.-NavigationActions ReactNavigation))
(defonce StackActions ^js/ReactNavigation.StackActions (.-StackActions ReactNavigation))
(defonce createMaterialTopTabNavigator (.-createMaterialTopTabNavigator ReactNavigation))
(defonce SafeAreaView (.-SafeAreaView ReactNavigation))
(defonce safe-area-view (r/adapt-react-class SafeAreaView))
(defonce navigator nil)
(defn dispatch
[^js/ReactNavigation.Navigation n a]
(.dispatch n a))
(defn set-state
[^js/ReactNavigation.Navigation n s]
(.setState n s))
(defn init
[m]
(s/assert ::navigator (:navigator m))
(set! navigator (:navigator m))
(when (:state m)
(set-state navigator #js {:nav (js/JSON.parse (:state m))})))
(defn reset
[]
(set! navigator nil))
(defn navigate
[{:keys [routeName params]}]
(s/assert ::navigator navigator)
(dispatch navigator (.navigate NavigationActions #js {:routeName (name routeName) :params params})))
(defn replace
[{:keys [key newKey routeName params]}]
(s/assert ::navigator navigator)
(dispatch navigator (.replace StackActions #js {:key key :newKey newKey :routeName (name routeName) :params params})))
(defn back
[]
(s/assert ::navigator navigator)
(dispatch navigator (.back NavigationActions)))
(defn push
[{:keys [routeName params]}]
(s/assert ::navigator navigator)
(dispatch navigator (.push StackActions #js {:routeName (name routeName) :params params})))
(defn pop
([]
(s/assert ::navigator navigator)
(.dispatch navigator (.pop StackActions)))
([{:keys [n]}]
(if n
(.dispatch navigator (.pop StackActions))
(.dispatch navigator (.pop StackActions n)))))
(defn state
[]
(s/assert ::navigator navigator)
(js->clj (gobj/get navigator "state") :keywordize-keys true))
(defn state-json
[]
(s/assert ::navigator navigator)
(js/JSON.stringify (gobj/getValueByKeys navigator "state" "nav")))
(defn route []
(let [{:keys [nav]} (state)]
(loop [{:keys [routes index] :as route} nav]
(if (int? index)
(recur (get routes index))
route))))
(defn params
"View helper for fetching route params from navigation object passed to screen"
[navigation]
(gobj/getValueByKeys navigation "state" "params"))
(s/def ::navigator
(s/and #(fn? (gobj/get % "dispatch"))
#(fn? (gobj/get % "setState"))
#(gobj/containsKey % "state")))
(s/def ::navigation-action #(instance? (type NavigationActions) %))
(s/def ::stack-action #(instance? (type StackActions) %))
The re-frame handlers are pretty simple.
The most interesting bit is the code which allows figwheel hot-reloading to work without resetting the navigation state. The :navigator/change handler keeps a serialised copy of the navigation state in the app-db. That state is provided when figwheel reload causes :navigator/init to be dispatched.
(ns mercury-app.cofx.navigator-cofx
(:require [interop.react-navigation :as react-navigation]
[re-frame.core :as re-frame]))
(re-frame/reg-cofx :navigator/state (fn [cofx _] (assoc cofx :navigator/state (react-navigation/state))))
(re-frame/reg-cofx :navigator/route (fn [cofx _] (assoc cofx :navigator/route (react-navigation/route))))
(ns mercury-app.fx.navigator-fx
(:require [re-frame.core :as rf]
[interop.react-navigation :as react-navigation]))
(rf/reg-fx :navigator/init react-navigation/init)
(rf/reg-fx :navigator/reset react-navigation/reset)
(rf/reg-fx :navigator/navigate react-navigation/navigate)
(rf/reg-fx :navigator/back react-navigation/back)
(rf/reg-fx :navigator/push react-navigation/push)
(rf/reg-fx :navigator/replace react-navigation/replace)
(rf/reg-fx :navigator/pop react-navigation/pop)
(ns mercury-app.events.navigator-events
(:require [re-frame.core :as rf]
[cljs.spec.alpha :as s]
[interop.react-navigation :as react-navigation]))
(rf/reg-event-fx
:navigator/init
[rf/debug]
(fn [{:keys [db]} [_ navigator]]
(s/assert ::react-navigation/navigator navigator)
{:navigator/init {:navigator navigator :state (:nav/state db)}}))
(rf/reg-event-fx
:navigator/reset
[rf/debug]
(fn [{:keys [db]} _]
{:navigator/reset nil}))
(rf/reg-event-fx
:navigator/change
[(rf/inject-cofx :navigator/route) (rf/inject-cofx :navigator/state-json)]
(fn [{:keys [react-navigation/route react-navigation/state-json db]} [_ {:keys [action-type]}]]
(case action-type
("Navigation/NAVIGATE" "Navigation/BACK" "Navigation/SET_PARAMS" "Navigation/POP" "Navigation/POP_TO_TOP" "Navigation/PUSH" "Navigation/RESET" "Navigation/REPLACE")
{:db (assoc db :nav/route route :nav/state state-json)}
nil)))
(ns mercury-app.nav
(:require [mercury-app.screen.welcome-screen :as welcome-screen]
[mercury-app.screen.loading-screen :as loading-screen]
[interop.react-navigation :as react-navigation]
[cljs.spec.alpha :as s]
[goog.object :as gobj]
[re-frame.core :as rf]
[reagent.core :as r]))
(def AppStack
(react-navigation/createStackNavigator
#js {"WelcomeScreen" #js {:screen welcome-screen/screen}}
#js {:initialRouteName "WelcomeScreen"}))
(def AppNavigator
(react-navigation/createSwitchNavigator
#js {"Loading" (r/reactify-component loading-screen/container)
"App" AppStack}
#js {:initialRouteName "Loading"}))
(def AppContainer (react-navigation/createAppContainer AppNavigator))
(defn handle-navigator-ref [navigator]
(if navigator
(rf/dispatch [:navigator/init navigator])
(rf/dispatch [:navigator/reset])))
(defn handle-navigation-change
[_ _ action]
(rf/dispatch [:navigator/change {:action-type (gobj/get action "type")}]))
(defn app-root []
[:> AppContainer {:ref handle-navigator-ref :onNavigationStateChange handle-navigation-change}])
Each screen needs to provide a react component, by convention we use a screen
var.
(ns mercury-app.screen.loading-screen
(:require [reagent.core :as r]
[interop.react-native :as rn]))
(def styles {:container {:flex 1 :alignItems "center" :justifyContent "center"}})
(defn container []
[rn/view {:style (:container styles)}
[rn/activity-indicator {:size "large"}]])
(def screen
(r/reactify-component container))