Skip to content
This repository has been archived by the owner on Aug 6, 2021. It is now read-only.

Navigating between screens

Oliver George edited this page Jun 2, 2019 · 4 revisions

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.

Decisions and Trade-offs

  • 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

Consequences

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.

interop.react-navigation

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) %))

Re-frame handlers

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)))

Configuring nav for the app

(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}])

Code required for each screen

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))