Skip to content

Instantly share code, notes, and snippets.

@jamesnyika
Last active April 7, 2024 01:06
Show Gist options
  • Save jamesnyika/2460f6f0e81304d92be1f23623e011fb to your computer and use it in GitHub Desktop.
Save jamesnyika/2460f6f0e81304d92be1f23623e011fb to your computer and use it in GitHub Desktop.
Using React Navigation 3 with Clojurescript

React Navigation in Clojurescript

I have been using v2 of React navigation for some time but needed to find a good article or source describing how to integrate it into a clojurescript project using Reagent, Re-frame, Shadow-cljs and no Leiningen or Figwheel. No offence to those libraries and tools, I just wanted to use Shadow so that I could also take advantage of the easy use of deps.edn. So how do you use React Navigation v3 in CLJS ?

Install Stuff

Make sure you have a project setup and install React Navigation using yarn or npm. I will not cover this. Please see this basic project setup from the shadow site as a reference :

Shadow CLJS Examples

The Basic Setup

Using React Navigation requires the definition of navigators, embedding the navigators in the app container and then exposing that to the screen.

Not to assume anything, here is the :require at the top of this page

(:require   [reagent.core :as r :refer [atom]]
            [dat.blocks :as b] ;; one of my own namespaces 
            ["react-navigation" :as nav]) ;; using shadow-cljs ability to use npm packages

Assumptions

  • You are using Expo - at least from SKD32 and on
  • You are using Shadow-cljs, the best clojurescript compiler at this time

Good Documentation References

Overview

One of the big changes from v2 to v3 of React Navigation is that it now requires that navigators be placed into another Navigtor container called the AppContainer. For a basic setup, this is probably the most significant change. We will show some of that here. This example really just works through handling a Stack Navigator but the same principles apply for other navigator types.

Step 1 : Define a set of screens that will be deposited in the navigators.

Below is a set of FORM-2 Reagent screens. The details of the actual controls is not important. Just that they are validly viewable screens

;; Each screen automatically gets props which includes navigation and state. These COME FOR FREE with react navigation.
(defn testscreen [{:keys [navigation state]}]
  (fn [{:keys [navigation state]}]

    [b/_view {:viewstyle {:flex "1" :alignItems "center" :justifyContent "center" :backgroundColor "blue"}}
      [b/_text {:style {:color "orange"}} "Hello James"]]
  )
)

(defn dashboard [{:keys [navigation state]}]
  (fn [{:keys [navigation state]}]
    (println "dashboard: state: " state)
    (println "dashboard: navigation: " navigation)
    [b/_view {:viewstyle {:flex "1" :alignItems "center" :justifyContent "center" :backgroundColor "#000"}}
      [b/_text {:style {:color "yellow"}} "Hello James"]]
  )
)

Step 2 : Define the screen stack

In Clojurescript, you just need to create a map here with a keyword representing the screen and then an inner map with the actual screen tied to a keyword :screen. Make sure you reactify the screen because you need to convert that clojurescript function into a JS function for interaction with the stackNavigator function.

 ;; 1. The :screen keyword is required as part of this definition
 ;; 2. Since this is going to be loaded INTO a navigator (a js object), you need to actually reactify it first.
 ;; this turns it into a react component (js style)
(def route-config {:login {:screen (r/reactify-component testscreen) }
                   :dashboard {:screen (r/reactify-component dashboard) }})

Step 3 : Grab the navigator creator function.

When you load React Navigation using Shadow-Cljs, you can use it directly because it is an NPM module. We load it up like so:

(ns dat.component.rnnav
(:require   [reagent.core :as r :refer [atom]]
            [dat.blocks :as b]
            ["react-navigation" :as nav])
  )
  ...

What is cool about this is that the navalias there is actually aliasing a HUGE object with all the React Navigation functions, constants etc. So we need to PULL out of that the functions that we need. To do that, use the form:

;; use def or defonce .. either one
;; DO NOT FORGET THE - (dash) / that is how you access a property on an object rather and without the dash - that would ;; be calling the function instead using Clojurescript interop.

(def createStackNavigatorFn (. nav -createStackNavigator))

So, here is how we do it

;;---------------NAVIGATORS AND DRAWERS-------------------
(defonce createStackNavigatorFn (. nav -createStackNavigator))
(defonce createSwitchNavigatorFn (. nav -createSwitchNavigator))
(defonce createTabNavigatorFn (. nav -createTabNavigator))
(defonce createDrawerNavigatorFn (. nav -createDrawerNavigator))
;;these will be wrapped in an App Container - React Navigation 3+
(defonce createAppContainerFn (. nav -createAppContainer))

Now that we have the functions available, we define the screens that will be contained within this stack navigator. to do this, just create a map with the screens and give the whole shebang a name.

 ;; 1. The :screen keyword is required as part of this definition
 ;; 2. Since this is going to be loaded INTO a navigator (a js object), you need to actually reactify it first.
 ;; this turns it into a react component (js style)
(def route-config {:login {:screen (r/reactify-component testscreen) }
                   :dashboard {:screen (r/reactify-component dashboard) }})

If you like, you can add additional Navigator Options, as another defined map which we will pass as a parameter to the stack navigator creator function. Here is one example for a stack navigator and others for other navigator types



;;Configuration information for the stack navigator. Includes defaults
(def stackNavigatorOptions {
                   :initialRouteName "login"
                   :headerMode "float"
                   :defaultNavigationOptions
                      stack-defaultNavigationOptions
                   :animationEnabled true

})

(def switchNavigatorOptions {
                   :initialRouteName "login"
                   :defaultNavigationOptions
                      switch-defaultNavigationOptions

})

(def drawerNavigatorOptions
      {
        :drawerWidth      300
        :drawerPosition   "left"
        :drawerBackgroundColor "#c0c0c0"
        :drawerType "front"
        :edgeWidth "10px"
        ;; content component takes a complete screen. No need to stack
        :contentComponent  (r/reactify-component dashboard)

    }
  )


Notice that there is a reference to defaultNavigationOptions in each of those maps. Those just happen to be additional maps - I mainly use them to fix my header behavior but you can learn more at the React Navigation Docs For Stack Navigators

Here are mine:

(def stack-defaultNavigationOptions
  { :headerStyle {:backgroundColor "#f4511e"}
    :headerTintColor "#fff"
    :headerTitleStyle {:fontWeight "bold"}
    :headerTitle "Some Header Title"
  })

  (def switch-defaultNavigationOptions
    { :headerStyle {:backgroundColor "#f4511e"}
      :headerTintColor "#fff"
      :headerTitleStyle {:fontWeight "bold"}
      :headerTitle "My Grand App Name"
    })

As you can see, they just change the background color and formatting for text

Step 4 : Assemble the pieces

Now we can assemble the final structure that will be passed to Expo (in this case) or React Native directly if you are doing things that way.

I create a function that will generate my final assembled contentComponent

;; :> means r/adapt-react-class function (it is a shorthand form)
 (defn approot []
    [:> (rnnav/make-app-container (rnnav/make-drawer-navigator  rnnav/stack-config rnnav/stackNavigatorOptions )) {}]
   )

Some salient points:

  • calling the make app container function returns a class. You must adapt it using Reagent's adapt-react-class (I used the shorthand form :> ) before using it.
  • pass in your stack config and then navigation options.

Step 5 : Pass to the registration function

Finally the stack can be rendered

;;the expo namespace below is actually something from
;; SHADOW-CLJS
(:require
 ["expo" :as ex]
 ["react-native" :as rn]
 ["react" :as react]
 [reagent.core :as r]
 [re-frame.core :as rf]
 [shadow.expo :as expo]
 ...



 ;; NOTE - THIS IS NOT AN EXPO FUNCTION
 ;; LOOK AT THE NAMESPACES ABOVE..
(expo/render-root  (r/as-element [approot]))

Done. Should see something wonderful.

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