Skip to content

Instantly share code, notes, and snippets.

@jamesnyika
Last active November 16, 2020 14:10
Show Gist options
  • Save jamesnyika/821282d6428c7248aa9f936c359c9ec7 to your computer and use it in GitHub Desktop.
Save jamesnyika/821282d6428c7248aa9f936c359c9ec7 to your computer and use it in GitHub Desktop.
React Navigation V3+ in Clojurescript

React Navigation is a great component for building mobile applications. It is versatile and supports both dominant platforms beautifully. However, despite the 2 libraries that exist out there to support this component in the Clojure ecosystem, there is sadly very little documentation on what and how you can set up and use this component. I could not get them to work for me (my failing) so I decided to try and make it work without the existing libraries just to that I can understand what is going on. Below is a laying out of my experience. Let me know if you have corrections so that we mortals who are not that sharp can learn.

React Navigation requires an exact set of steps to make it successfully work

Step 1: Installation of React Navigation

Use yarn to add the library to the project

yarn add react-navigation

You also need to then include it within the project like so

;;---------------Def React Navigation ----------
(defonce ReactNavigation (js/require "react-navigation"))

TIP: I usually just use a def, but I have seen plenty of people use defonce.

TIP: Many people will also run assert on these vars. Maybe you should too. Its good for you. Actually, why don't you spec the whole thing while you are at it ?

Step 2: Configure and set up the stack navigator

The stack navigator takes two arguments (or one.. but pass both. The second can be set on the stack navigator object later). Create a function for initializing stack navigators

Key features to notice

  • Since this is a javascript class, you are going to have to transform the map values passed in to JS format using clojurescript interop
  • Once created, run reagent's adapt-react-class to get an adapter class.

Once done this part is ready. What you have done here is create a reusable function for creating stack navigators. You have not yet created an actual a stack navigator. We will do that very soon.

;;this function takes a route configuration and navigator options and sets up an actual stack navigator you can use
(defn make-stack-navigator [route-configs navigator-configs]
  (r/adapt-react-class (StackNavigator (clj->js route-configs) (clj->js navigator-configs))))
  

Step 3: Helper setup

Stack navigators will require additional work done to your screens. There is a very important screen wrapper function I have found and used that will wrap normal screens with the necessary parameters to make it show up in the navigator.

;;--------------------------------------------------------------------------------------
;; this helper takes navigation info anse sets it on the state variable.
;; setting this information on the navigation object allows the navigator to know what to show

(defn navigation->state [navigation]
  (-> navigation .-state (js->clj :keywordize-keys true)))

;;--------------------------------------------------------------------------------------
;; useful fuction for turning js maps without keywords into something we can destruct
;; I only just noticed that it is exactly what is used in the navigation->state method. 
;; you can replace it if you like. 

(defn keywordize
  [obj]
  (when obj
    (js->clj obj :keywordize-keys true)))

;;--------------------------------------------------------------------------------------
;; this is the actual wrapper function. It uses the method above. I have documented
;; it inline

;; this method takes a simple screen that uses expo or nativebase or whatever you like 
;; from the react native world. Remember you can also pass in components that actually are returning
;; a function
;; The function requires:
;;   - the component 
;;   - a map with a title for the screen, something to put on the left and right, tab labels or 
;;     tab icons and a background color for the header. If you do not supply some, you can see what we
;;     supply as default values. 
(defn wrap-navigation [component {:keys [title
                                         left
                                         right
                                         tab-label
                                         tab-icon
                                         background-color]
                                  :or {title (fn wrap-navigation-title-fn [a b] (str "title" a))
                                       left nil
                                       right nil
                                       tab-label nil
                                       tab-icon nil
                                       background-color "orange"}}]
                                       
  ;;we create a new var to hold the reactified version of this component you pass in. it is held in the form of a function
  ;; that can take a map which includes the navigation object from the stack navigator. We need this to be able to get to the 
  ;; goBack and navigate functions. We also need it to set the state variable inside the navigation object. 
  ;; so you will that the function returns a react native component in the form of :
  ;; [component navigation navigationOptions]
  (let [c (r/reactify-component
            (fn wrap-navigation-r [{:keys [navigation]}]
              [component navigation (navigation->state navigation) ]))]
              
    ;;but wait - there's more. We then set a variable called navigationOptions on the newly reactified component and again
    ;;return a function that accepts the navigation object merges in with existing preset options any change you had 
    ;;supplied in the top level parameters for title, left, right etc. Notice the little 'c' at the bottom. We return that
    ;; reactified screen 
    (aset c "navigationOptions"
          (fn wrap-navigation-fn [navigation]
            (clj->js (merge {; :tabBar (fn wrap-navigation-tab-bar-fn [navigation]
                             ;           (let [state (navigation->state navigation)]
                             ;            (clj->js
                             ;              {:label (if tab-label (tab-label state) "-EMPTY-")})))
                             ;               ;:icon (if tab-icon (tab-icon state) nil)})))
                             :title   (let [state (navigation->state navigation)
                                            {:keys [params routeName]} state]
                                        (if title (title state) nil))
                             :headerTintColor "#FFF"
                             :headerTitleStyle {:color "#FFF"
                                                :marginLeft 30}
                             :headerStyle {:backgroundColor background-color}}
                            (if left {:headerLeft (left (navigation->state navigation))} {})
                            (if right {:headerRight (right (navigation->state navigation))} {})))))
    c))


Step 4 Create Some Screens to Stack

We add two screens as an example. The createQuote screen is correctly formed to be able to take in navigation items All your screens should

  • Take a navigation object at the top level
  • Pass the navigation object to the function the component returns.
  • Inside the returned function you can get a hold of the objects you need to navigate including
    • navigate
    • goBack
    • state
    • dispatch
    • setParams
    • title

In this example below, I am only destructuring navigate and goBack. I then USE navigate so that I can go to another screen if the button is pressed. The beauty of navigators like this is that when they navigate, they will give you a link in the header automatically to return where you came from. This is actually your goBack function.

The actual screen can be as light or heavy as you like. It is all up to you In this example, I am actually using components from the Expo and NativeBase libraries Works beautifully.

I have a lot of buttons in there because when I was testing the screen out, I was paranoid that I would not see the button if I only put one and that the header would cover it. Turns out that everything will show up below the header!

(defn _createQuote [navigation]
  (fn [navigation]
    ;;(util/log "----inner nav is " (util/keywordize navigation) "----")
    (let [{:keys [navigate goBack]} (util/keywordize navigation)]
     ;; (util/log "------navigate is " navigate)
    [ui/container {:background-color "#fff"}
     [ui/content {:padder true}
      [ui/button {:onPress #(navigate "ViewQuote")} [ui/text "CreateQuote"]]
      [ui/button [ui/text "CreateQuote"]]
      [ui/button [ui/text "CreateQuote"]]
      [ui/button [ui/text "CreateQuote"]]
      [ui/button [ui/text "CreateQuote"]]
      [ui/button [ui/text "CreateQuote"]]]]
    ))
  )


(defn _viewQuote [props]
  ;;return a function
  (fn [{:keys [navigation]} props]
    [ui/container {:background-color "#a4b3d6"}
     [uic/_header {:tagline "Managing Your Business"}]
     [ui/content {:padder true}
      ;; this body tag has the effect of constraining the width of buttons. Remove at will
      [ui/button [ui/text "joker button"]]

      ]
     [uic/_footer]]
    ))

Step 5 Create the stack configuration. Not the stack. Just the config.

We now have two screens we can use. Let's stack them. You do this by creating a map of the format

{:Screen1Key {:screen (wrap-navigation screenfn navigationOptionsMap)}
  :Screen2Key {:screen (wrap-navigation screen2fn navigationOptionsMap2)
  :Screen3Key {:screen (wrap-navigation screen3fn navigationOptionsMap)}
  ...}

  • Screen1Key can be any keyword. eg: :login or :LoginScreen or :MySpecialScreen or :myspecialscreen
  • You must use the :screen keyword to identify which function returns your screen. Here is where the previously defined function for wrapping screens is used. It taks your screen function (in my case (defn _createQuote...)) and adds all the nice stuff required for it to show up in the navigator. My definition is below: (notice i have put things in different namespaces hence the ui/whatever and scr/whatever ...)

(def stacknav {:CreateQuote {:screen (ui/wrap-navigation scr/_createQuote {:title (fn [] "My Title")})}
               :ViewQuote {:screen (ui/wrap-navigation scr/_viewQuote {:title (fn [] "ViewQuote")})}
               })

Step 6: Lets use the stack navigator now.

In your core file or whereever you bind react native in follow the is pattern

  1. Now really create the stack. Some folks like to take the second param map and put it separately. No problem there. Do it if it makes it easier for you to read your code
(def app-stack (ui/make-stack-navigator router/stacknav {:initialRouteName "CreateQuote" :headerMode "float" }))

  1. Define you application root. here is how I did mine.
  • Notice that make-stack-navigator which was defined in step 2 has already adapted the Stack Navigation object itself. That means you can use it like any other component which is what we do here. But always - return a function that can be called and not the component directly.
(defn app-root []
  (let [current-screen (subscribe [ :screen/current-screen])]
           (fn []
             [app-stack]
               )))

  1. Bind

Make it real.

....
      (dispatch-sync [:db/initialize-db])

      ;;somewhere after dispaatch you the create your app and register it. 
      (.registerRootComponent mod/Exponent (r/reactify-component app-root))
      
      ;; I mentioned that I use exponent so I use this format above. This older format below here also works. 
      ;; Make cool-app-name anything you like
      ;; (.registerComponent ui/app-registry "-cool app name -" #(r/reactify-component app-root))

And now you should be able to open your app in your simulator and see the navigation at work showing your top screen and the top button on the create quote can be clicked to 'navigate' with nice animation to the viewQuote screen.

@kangbb
Copy link

kangbb commented Aug 13, 2019

It's a quite awesome scheme to use react navigation, which really help me a lot. How do you get it? I'm learning react native and clojurescript too, also want to get reference to learn usage of react navigation. Can we do this with reagent/react-react-class, which is consistent with createReactClass in React?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment