Cleaner ClojureScript React Native Interop

July 17, 2017

Exciting changes have landed in ClojureScript master and an unexpected side-benefit of ClojureScript is not an Island: Integrating Node Modules is improved interop between ClojureScript and React Native.

Credit to António Monteiro for much of the hard work behind this feature, and discovering its applicability to React Native!




Let's take a look at how we can take advantage of this new capability to dramatically simplify the code for a re-natal-based React Native app. To keep things simple, let's just focus on the template app that is produced by running re-natal init FutureApp.

In the future-app.ios.core namespace, there are a couple of lines that read:

(def ReactNative (js/require "react-native"))
(def app-registry (.-AppRegistry ReactNative))

This gets compiled down to JavaScript that initializes the Vars being defined. It makes use of require, which is used by the React Native packager and Node to load the needed code.

future_app.ios.core.ReactNative = require("react-native");
future_app.ios.core.app_registry = future_app.ios.core.ReactNative.AppRegistry;

With this understanding in place, let's remove the ReactNative Var definition and instead add a spec to :require in our ns form:

(ns future-app.ios.core
  (:require [reagent.core :as r :refer [atom]]
            [re-frame.core :refer [subscribe dispatch dispatch-sync]]
            [future-app.events]
            [future-app.subs]
            [react-native :as ReactNative]))   ;; <- This was added

Additionally, to take advantage of the new compiler capability, add

:target :nodejs

to your compiler options.

If you try to compile this with the currently-released ClojureScript compiler, you will get an error:

No such namespace: react-native, could not locate react_native.cljs, react_native.cljc, or Closure namespace "react-native"

But, if you instead use the compiler currently on master (this post was written using 1.9.813), the code will compile and you will be able to run the resulting app.

If you look at the generated JavaScript, the two lines generated above have been changed to the following JavaScript, which effectively behaves in the same way:

future_app.ios.core.node$module$react_native = require('react-native');
future_app.ios.core.app_registry = future_app.ios.core.node$module$react_native.AppRegistry;

In fact, the app-registry Var definition should now be revised to the simpler

(def app-registry react-native/AppRegistry)

and the same JavaScript will be produced. Alternatively, you could add :refer [AppRegistry] to the :require spec for react-native and then AppRegistry can be used in lieu of react-native/AppRegistry as you'd expect.

The two ideas here are

  1. Use the ns form instead of explicit js/require constructs.
  2. Employ aliases, refers, qualified symbols, and interop as usual to simplify your code.

The cruft near the beginning of the sample FutureApp namespace disappears, and you are left with clean code:

(ns future-app.ios.core
  (:require [reagent.core :as r :refer [atom]]
            [re-frame.core :refer [subscribe dispatch dispatch-sync]]
            [future-app.events]
            [future-app.subs]
            [react-native :as rn]))

(def text (r/adapt-react-class rn/Text))
(def view (r/adapt-react-class rn/View))
(def image (r/adapt-react-class rn/Image))
(def touchable-highlight (r/adapt-react-class rn/TouchableHighlight))

You can even use simple dot interop forms to refer to nested entities. Examples:

  • rn/Animated.View for a nested component
  • rn/Animated.timing a nested function

You can use these techniques to simplify access to 3rd party components you may be using in your app. For example, you can (:require [react-native-color-picker]) to simplify working with react-native-color-picker. This doesn't magically set up the component—you still need to follow the instructions at Using external React Native Components, but it cleans up access to it in your code.

All of the above van be viewed as (very nice) syntactic sugar for working with React Native. It doesn't change the compilation model: Externs are still needed if compiling with :advanced and since the React Native packager is still used, the React and React Native JavaScript code is not passed through the Closure compiler (perhaps that's a job for another day!)

I'd encourage you to give ClojureScript master a try with your React Native ClojureScript project and see if using :target :nodejs can simplify things for you!

Tags: React Native ClojureScript