"Upgrading" the react-native support

This is really great news! My company already uses shadow-cljs for our web-facing code, and the tail end of 2019 is for us The Year of Linux on the Desktop fixing our long-suffering, still ExpoKit-encumbered React Native project into something we can stand to maintain long-term. I will be watching developments with interest, and actively seeking to beta-test, help debug, etc. as appropriate. Cheers!

1 Like

Hmm, now I’m getting unsure. I thought that the spec of require allowed for dynamic strings (as opposed to import). But I’m very possibly not correct.

LTTTA - Less things to think about - wins in my book. Compiler magic would be awesome.

FWIW I voted for option 2 (compiler macros) because there’s already, in my experience, a “okay now go do some manual work or it won’t build/won’t build in production mode” step in React Native work. Maybe that’s not true for everyone, but for my project which is still encumbered by an early decision to adopt ExpoKit, the need to produce several build variants (QA build lane for TestFlight, white-labeled app version, other reasons), etc., etc. I don’t think that it’s worth making too many structural compromises for developer convenience. Developer life in the React Native universe is going to have some unbuffed burrs and manual checklist-y things regardless of what shadow-cljs does or does not provide.
Why then not vote for Option 1? Simple; I expect that in the fullness of time this macro form might be the entry point for additional goodness that lets us get real source maps all the way out to YellowBox and RedBox, better reloading, and so forth. I don’t mind putting it in my code, I hope to be choosing a platform for the “long haul” (at least another 2-3 years) and so adopting some local dialect is not a huge cost, amortized over that long a period of hopefully-stable development.

I find it difficult to decide which one I like best. I know the one I like LEAST: (1). I think duplication should be avoided if at all possible, it is very confusing and error prone.

The ideal solution should “just work” the way people would expect. So how is that exactly? For me as somebody new to Clojure(Script) altogether, I was expecting something like option (4) until I learned that I had to use js/require, so for me that would be an intuitive solution. But people coming from non-shadow CLJSRN systems, this may be different.

I agree with the sentiment to be “hesitant to adding shadow-cljs only stuff”, but it seems that the better options all require this. I want to vote for (2), (3) and (4) as in: “anything that is not (1)”. Since that is not possible, I’m going to vote for option (3) because it does not involve duplication and it is the most popular non-(1) so has the best chances of gaining the lead :slight_smile:

By the way, are you aware of the mechanism used by Metro that decides on which files to treat as assets (https://facebook.github.io/metro/docs/en/configuration#assetexts)? I was thinking that in option (4) you could (should?) hook into that to automatically determine which of the ns-requires are assets and which are not…

By the way I’m really happy for you @thheller that you got the funding and it’s awesome that you’re involving us like this, thanks!!

1 Like

After some more investigation Option #4 is pretty much out since it it is important to maintain the “when” something is used. So the location where js/require is used may be relevant as much as what it actually required.

I have half implemented Option #3 and it seems like the most reliable option since it will automatically work if someone decides to build macros on top. The js/require is extracted during compilation from the actual AST so it should be pretty reliable. Repeating things in the config (ie. Option 1) seems entire unnecessary then.

3 Likes

Too bad there isn’t an instant runoff voting mechanism. I was pretty conflicted between 1 & 3.

1 Like

I just released shadow-cljs version 2.8.44 which contains a basic version of Option #3 so that all js/require calls should now be automatically extracted and just work without the need for additional configuration. They still have to be fully static though. Can always add manual configuration for additional requires later if needed.

Source maps should be working semi-reliable as long as Chrome is used but I expect there to be some quirks so please help me test this.

I added another feature which in theory should allow code-splitting react-native builds and when combining this with the “RAM-bundle” feature of metro should possibly improve startup performance. I’ll probably provide more details on this is a separate post once I had a bit of time to actually test it properly. If you’d be interested in helping me test this let me know. The bigger the app the better. :wink:

7 Likes

I haven’t actually done React native stuff, but what if we bypassed the whole source map issue altogether. If the problem is debugging, could a mode which instruments every single call be used? Like a try around everything that records the source location, and on error reports it. I guess it wouldn’t support full breakpoints and debugger tools in browser. But it would let us know where the issue occurred.

It’s almost some kind of ad-hoc inline source map as instrumented code. Don’t know if that’s easy or possible or useful.

Pretty sure that would make things unusably slow without providing any actual benefit.

Debugging shouldn’t be a problem when using Chrome. Pretty sure that is the only way to debug anything anyways so thats fine.

Wow, this is great news! I’ll try it out in a toy project later today and then start a conversation in the team about trying it out in our production app. I’m definitely interested in the code splitting / startup time idea - do you know if it require your app to be structured in a specific way?

Yep - debugging in chrome is suprisingly good when source maps are enabled - you get the line in the source mapped file highlighted where the error originates, then all the stack frames are clickable, so you can move around the call tree. It even in many cases will also have the relevant locals available so if you add variables to the watch list in the side panel you will see what value they had - pro tip call clj->js on clojure variables so you can drill down into maps etc.

1 Like

Code-splitting in general requires apps to be structured a little differently yes. You basically just delay requiring something until you actually need it. So in the case of react-native you’d use the appraoch described in the performance docs and apply that directly to the CLJS setup you use.

In there example a require is executed when a button is clicked which then initializes the component that is later used in render.

  didPress = () => {
    if (VeryExpensive == null) {
      VeryExpensive = require('./VeryExpensive').default;
    }

    this.setState(() => ({
      needsExpensive: true,
    }));
  };

The same thing can now be done in shadow-cljs via a bit of extra config.

  {:target :react-native
   :init-fn demo.rn/init
   :chunks {:foo demo.rn-foo/component}
   :output-dir "app"}

Note the extra :chunks config which will create a app/foo.js in additional to the normal app/index.js. So you :init-fn will be executed normally on startup and you can dynamically (js/require "./foo.js") in the code which will return whatever the qualified symbol refered to.

(ns demo.rn-foo)

(defn component [] ...)

So in the above example you’d get the component fn returned by the js/require. You can return anything here (eg. a map via (def component {:whatever true})).

Code-splitting is kinda hard to describe in a general way since it is usually very specific to each app and there is no generic “best” approach that works for everything. The idea I guess for react-native would be to “delay” loading specific screens of an app until they are actually needed. I can’t say how much of a benefit this actually is since I only did the JS side of things and didn’t investigate the whole RN “RAM-bundle” further yet. But according to their docs this should work.

I wrote more about code-splitting in the browser here. You just use js/require instead of the described shadow.lazy stuff.

1 Like

I just started trying shadow-cljs + react-native today, so far it has been among the least painful cljs-setups I’ve tried to do. Awesome job! :smiley: These changes sounds like they’ll make it even more pleasant to use.
BTW would you like a pull-request for your react-native example? I just updated some dependencies in order for expo start --web to work.

3 Likes

Sounds good. I didn’t get a chance to test that yet but was planning to.

Is the upgrade support both :react-native and :expo target?

No, the expo target is deprecated and should no longer be used. Expo should work fine with the :react-native target.

1 Like

@thheller Just used source maps and the react native debugger to fix an issue upgrading expo-sdk to v34. Thanks for all the hard work!

2 Likes

This topic was automatically closed 182 days after the last reply. New replies are no longer allowed.