TOMER WELLER / A BLOB

Clojurescript/Reagent : importing React components from NPM

This post describes, step by step, how to setup a workflow for importing React components from NPM, using Webpack, and incorporate them in your Reagent views.

UPDATE (June 17th, 2017): A year later, this post and method are very much still relevant. I’ve made a small update to take advantage of the :foreign-libs option, as suggested in the comments (Thanks Andreas and Alex!), which reduces the plumbing necessary.

Motivation

Any Javascript developer using a modern build tool can easily test and incorporate React components from 3rd party developers in their app. It’s usually just a matter of declaring dependencies, building and importing.

Moving to Clojurescript and Reagent has been amazing in many ways but I just couldn’t wrap my head around a comparable import flow.

The workflow I will describe employs NPM as a dependency manager and Webpack as a build tool. The build artifact is added as an external Javascript dependency to an otherwise (pretty) standard Leinnnigen build process.

This solution does not use Cljsjs in anyway.

Why not Cljsjs?

Cljsjs is a great community effort to package common Javascript libraries in a Clojurescript friendly way. However, it has some shortcomings.

  1. Very few packages compared to NPM or Bower. Whenever I look for something it’s usually not there.
  2. Packages are mostly out of date compared to their NPM counterparts. This make sense because with NPM packages the author/maintainer of the library is the same person as the maintainer of the package. That’s not the case with Cljsjs.
  3. Packaging a library is not trivial. The community’s usual reply to the import issue is “just package it yourself”. Sending someone who wants to fiddle around with a 3rd party react component, to package the dependency themselves is, in my opinion, cumbersome. This also leads back to problem #2.

Zefstyle

In this example we’ll start with a simple frontend-only Reagant app and setup a workflow that will allow us to add a react-player component to it. Let’s call this project Zefstyle. You can also skip this altogether and just jump to the finished result here.

Starting with a template:

$ lein new reagent-frontend zefstyle && cd zefstyle

For sanity - Let’s see that it’s working

$ lein figwheel

While that’s running, open public/index.html in your favorite browser. You should see a page that says “Welcome to Reagent”

Since we’re running figwheel, we can change that live. In public/index.html change the header to “Zef Style” - You should see it updating within seconds. You can then close fihghweel for now.

NPM Setup

Let’s create a minimal package.json file. We can either do it interactively with npm init or just manually create a package.json file:

{
  "name": "zefstyle",
  "version": "0.0.2",
  "author": "tomer.weller@gmail.com",
  "scripts": {
    "watch": "webpack -d --watch",
    "build": "webpack -p"
  },
  "dependencies": {
    "react": "15.5.4",
    "react-dom": "15.5.4",
    "webpack": "1.13.1",
    "react-player": "0.18.0"
  }
}

package.json remarks:

  • We import react and react-dom although they are not a hard dependency of react-player and more than that - we already have a React instance in our code, the one that Reagant depends on. Why? There is an order of execution issue when relying on Reagant’s React dependency. This also means that we will later need to exclude Reagant’s React dependency so that we don’t have two React instances.
  • I’ve included some shortcuts for webpack scripts. They are not necessary but will come in handy.

Install dependencies:

$ npm install

All of our dependency tree should now be in the node_modules directory.

An alternative approach to this step would be to use the lein-npm plugin. I’m pretty comfortable with npm so I decided to do it myself.

Webpack setup

Our webpack setup will be pretty minimal - no fancy loaders. It consists of a definition file webpack.config.js and an entry script that bootstraps our imported libraries to the window object.

webpack.config.js :

const webpack = require('webpack');
const path = require('path');

const BUILD_DIR = path.resolve(__dirname, 'public', 'js');
const APP_DIR = path.resolve(__dirname, 'src', 'js');

const config = {
  entry: `${APP_DIR}/main.js`,
  output: {
    path: BUILD_DIR,
    filename: 'bundle.js'
  },
};

module.exports = config;

We basically defined our entry point script as src/js/main.js and our output artifact as public/js/bundle.js.

Our entry src/js/main.js should look something like:

window.deps = {
    'react' : require('react'),
    'react-dom' : require('react-dom'),
    'react-player' : require('react-player'),
};

window.React = window.deps['react'];
window.ReactDOM = window.deps['react-dom'];

I usually prefer not to bind too many objects to the global window context, that’s why I push whatever I can into window.deps. With that said, React and ReactDOM must be on the global window context because that’s where components expect them to be.

Now we can either run

$ npm run build 

For a onetime build, or:

$ npm run watch

For a continuous build that watches for changes in our code. Given that the Javascript code should remain pretty static the watch might be redundant.

Leiningen project setup

Like I mentioned before, we’re counting on webpack to bring React into the picture. That means that we need to get rid of the cljsjs.react & cljsjs.react.dom packages that reagant depends on.

In project.clj let’s change our dependencies to be:

:dependencies [[org.clojure/clojure "1.8.0" :scope "provided"]
             [org.clojure/clojurescript "1.9.562" :scope "provided"]
             [reagent "0.6.2" :exclusions [cljsjs/react cljsjs/react-dom]]]

And add an external dependency to our webpack generated bundle.js through :foreign-libs. We need to add the following to all build profiles:

:foreign-libs [{:file "public/js/bundle.js"
                :provides ["cljsjs.react" "cljsjs.react.dom" "webpack.bundle"]}]

Notice how our foreign lib satisfies all react dependencies and also add a convenience namespace, webpack.bundle, for importing the external bundle.

At this point it’s a good idea to cleanup and re-run figwheel.

$ lein clean && lein figwheel

Grand finale

Now that we have everything in place we can add the react-player to our main Reagant view.

in src/zefstyle/core.cljs let’s add the webpack.bundle dependency to our namespace:

(ns zefstyle.core
  (:require [reagent.core :as reagent :refer [atom]]
            [webpack.bundle]))

and change our home-page function to be:

(defn home-page []
  (let [react-player (aget js/window "deps" "react-player")]
    [:div
     [:h2 "Zef Style"]
     [:> react-player {:url "https://youtu.be/uMK0prafzw0"}]]))

Notice the special :> syntax for using pure React components.

opening public/index.html should yield something like this: yolandi

Voila! We brought the pure JS react-player component into our Reagent view.

To bring in new components we just need to declare them in package.json, require() them in main.js and rebuild.

Caveats:

  1. JS dependencies are not processed by the Closure compiler so they do not get advanced optimization, only whatever webpack is setup to do.
  2. Setting up this process is not trivial. However, it only needs to be done once and from then on it’s a smooth sail. Also, automating the process or part of it sounds like a feasible task as part of a lein plugin. I’ll look into that when some time frees up.

Let me know if this was helpful, requires some fixups or if it’s complete rubbish.

Thanks to: