Just-in-Time Script Loading With React And ClojureScript

In the last projects I've been working on I've come accross the situation that I needed to load some external script (Stripe, Google Maps, ...) at a certain point and then do something with the features exposed by this newly imported library. Some times you might be able to circumvent loading a library at runtime by bundling it with your main application but even then you might want to consider splitting it into a separate module and loading it when it's actually needed.

We won't talk about module splitting and loading in this blog post though and instead focus on loading things like Stripe and Google Maps that just can't be bundled with your application.

The easy way to load these would be using a simple script tag:

<script type="text/javascript" src="https://js.stripe.com/v2/"></script>

With this approach however you load the script for every user even though they may never, or already went through, your payment flow. A better way would be to load it when the user actually wants to pay you. I've heard fast loading apps make that more likely as well ;) Also you might say that these scripts could be cached, but even if they are: you still pay for the parsing and execution time.

So how can we go about that? What follows is one pattern that I think is fairly simple and elegant and also a nice use of React's lifecycle features and higher-order components:

(ns your-app.lib.reagent
  (:require [reagent.core :as reagent]
            [goog.net.jsloader :as jsl]))

(defn filter-loaded [scripts]
  (reduce (fn [acc [loaded? src]]
            (if (loaded?) acc (conj acc src)))
          []
          scripts))

(defn js-loader
  "Load a supplied list of Javascript files and render a component
   during loading and another component as soon as every script is
   loaded.

   Arg map: {:scripts {loaded-test-fn src}
             :loading component
             :loaded component}"
  [{:keys [scripts loading loaded]}]
  (let [loaded? (reagent/atom false)]
    (reagent/create-class
     {:component-did-mount (fn [_]
                             (let [not-loaded (clj->js (filter-loaded scripts))]
                               (.then (jsl/loadMany not-loaded)
                                      #(do (js/console.info "Loaded:" not-loaded)
                                           (reset! loaded? true)))))
      :reagent-render (fn [{:keys [scripts loading loaded]}]
                        (if @loaded? loaded loading))})))

And here's how you can use it:

;; payment-form can expect `js/Stripe` to be defined
[js-loader {:scripts {#(exists? js/Stripe) "https://js.stripe.com/v2/"}
            :loading [:div "Loading..."]
            :loaded [payment-form]}]

So, what can we take away from this besides the specific snippets above?

  • Higher order components can be very useful to hide away side effects needed for your views to function
  • They also are perfectly reusable
  • You can of course also use higher order components to pass things into child components, we don't do that here but if you create some stateful object this may come in handy

Hope this is helpful — let me know if you have any thoughts or suggestions :)

Other Posts

  1. Props, Children & Component Lifecycle in ReagentMay 2016
  2. Om/Next Reading ListNovember 2015
  3. Parameterizing ClojureScript BuildsAugust 2015
  4. ClojureBridge BerlinJuly 2015
  5. Managing Local and Project-wide Development Parameters in LeiningenJune 2015
  6. Formal Methods at AmazonApril 2015
  7. (lisp keymap)February 2015
  8. CLJSJS - Use Javascript Libraries in Clojurescript With EaseJanuary 2015
  9. Why Boot is Relevant For The Clojure EcosystemNovember 2014
  10. S3-Beam — Direct Upload to S3 with Clojure & ClojurescriptOctober 2014
  11. Patalyze — An Experiment Exploring Publicly Available Patent DataOctober 2014
  12. Running a Clojure Uberjar inside DockerSeptember 2014
  13. Using core.async and Transducers to upload files from the browser to S3September 2014
  14. Emacs & VimJuly 2014
  15. Heroku-like Deployment With Dokku And DigitalOceanMarch 2014
  16. Woodworking MasterclassesFebruary 2014
  17. Early Adopters And Inverted Social ProofFebruary 2014
  18. Living SmallFebruary 2014
  19. Sending You a TelegramJanuary 2014
  20. Running a Marathon, Or NotJanuary 2014
  21. Code SimplicityJanuary 2014
  22. What do we need to know?December 2013
  23. Sculley's DiseaseDecember 2013
  24. A Resurrection PostDecember 2013
  25. A Trip To The USSeptember 2013
  26. Analytics DataApril 2013
  27. Asynchronous CommunicationApril 2013
  28. From Zero to Marathon in Six MonthtsMarch 2013
  29. Git Information in Fish Shell’s PromptDecember 2012
  30. When We Build StuffAugust 2012
  31. Models, Operations, Views and EventsJuly 2012
  32. The Twelve Factor AppJune 2012
  33. Paris And BackMay 2012
  34. A Friend Is Looking For A Summer InternshipMay 2012
  35. Kandan Team ChatMay 2012
  36. Startups, This Is How Design WorksMarch 2012
  37. Entypo Icon SetMarch 2012
  38. Hosting A Static Site On Amazon S3February 2012
  39. Exim4 Fix Wrongly Decoded Mail SubjectJanuary 2012