Packaging your React components
Using Rails, what you natively get is Sprockets, which I feel doesn’t provide the same robustness and flexibility CommonJS does.
So how could I continue using modules within Rails?
This article will quickly go through the required steps to use browserify-rails along with react-rails, so that our React components can be packaged and required as CommonJS modules, while maintaining the ability to render them on the server.
I am assuming you have at least a basic knowledge of Rails, but will detail the Node.js/modules parts a bit more in case you are not familiar with those.
Rails & Browserify
Browserify is an NPM package, and will allow us to use other NPM packages as well, making them available to our client-side scripts. Which means you are going to need Node.js on your development machine to make it all work.
Download and execute Node’s installer from the website before going further.
To use NPM with our Rails app, we need to make it an NPM package too, so that we can list our dependencies and manage them with the NPM command-line tool.
An NPM package is described by a package.json file, which you are going to create inside the root directory of your Rails application:
This JSON file defines our package’s name, some basic metadata and the other packages we depend on for development, which for the moment only includes Browserify. The browserify-incremental tool is used by the newest version of the browserify-rails gem to make JS packaging faster when your source code changes.
Let’s install it within our app:
This command looks for package.json in the current directory, lists the declared dependencies and installs whichever one could not be found in the local node_modules directory.
As it is the first time we run it in our Rails app directory, it will create node_modules and install the browserify package there.
Add the gem to your Gemfile and run bundle install:
The Greeter module
We’re going to write a very simple module that will publish two functions: sayHello and sayGoodbye.
What’s important here is the assignment to module.exports. This is how you declare which functions, objects, variables (…) are exported by your module, and this is what browserify-rails will look for to decide whether it should run browserify on your file or not.
Note that the exported name might be different than the original function/object/… name from within the module code: here, we publish the hello function as sayHello and the goodbye function as sayGoodbye.
Here’s how you can require and use the module in your main application.js file:
It couldn’t be easier! As you can see, you can still use the Sprockets require directive too.
What’s worth mentioning here, is how the use of CommonJS modules allow us to give a meaningful and contextual name to our imported pieces of code. The Greeter variable could have been named MyModule, and it wouldn’t have made any difference.
Also, you’ve probably realised that Greeter actually took the value we had assigned to module.exports in our module, which is a simple object. It means we can also import only the parts we really need from a module. For instance:
This time, we’ve only imported the sayHello function, which is renamed to hello within this part of the code.
Rails, React & Browserify
In order to make everything work together, we need to tweak a few things. But first, let’s list our goals:
- Maintain compatibility with the React UJS feature from react-rails, which mounts the appropriate React components on the client side when it finds specific tags output by the react_component view helper.
- Maintain compatibility with the server-side rendering feature of react-rails, which allows us to render React components on the server so that a complete HTML markup is served.
Points 2. and 3. might be clearer to you if you have read my previous article.
JSX files to JS modules
Thankfully, Browserify comes in with a very powerful feature: transformers. A transformer is similar to an engine in the Sprockets jargon: it takes a file in, transforms it and outputs it so that it can be processed further.
As you have probably guessed already, there is a browserify transformer that will take care of JSX files for us. It’s an NPM package called reactify.
Let’s add it as a dependency in our package.json file:
Once it’s installed, we need to tell browserify-rails about that extra option we’d like it to use when running browserify. This is done by adding the following line inside config/application.rb:
There are actually two options here. The first one tells browserify to use the reactify tranformer. The second one tells browserify to treat files with the .js.jsx extensions as modules, so that we can require such files without specifying their extension all the time.
Our first requirement is met: React components can now fully be bundled into modules.
Components in the global scope
To fulfill our two other goals, there’s just one thing to know: React will look for our components in the global scope.
Say we’re trying to render a <Page /> component. In a browser, React will try to use window.Page as a React component and mount it. On the server, where window doesn’t exist, it would rather look for something like global.Page.
The simplest solution to this is to declare components as global variables by omitting the var keyword:
This will make MyComponent available to the global scope, be it window, global or anything else. But I don’t like polluting the global scope without at least showing that I know what I am doing. I’d really prefer writing something like this:
For this to work, we’ll need another tweak.
In react-rails, the server-side rendering code only declares a global variable that represents the global scope. Thus, any component assigned to a property on the window variable won’t be found.
On the other end, browsers use window to represent the global scope and do not usually define global.
To work around this, the dummy app from the react-rails test suite manually declares the global variable so that it will take the value of window if it is not defined already:
A component can then be created on the global variable and be accessible both to the server (which defines global) and the client (which defines window).
So this is a way to go. However, I’d rather have nothing to write, so I have sent a pull request to fix this on the gem side by defining global, window and self in the code that serves as the context for server-side rendering.
Until the PR is merged or rejected (hopefully for a good reason), I will personally use this line in my Gemfile to avoid the above workaround:
Feel free to choose whichever solution makes sense to you!
My pull request has been merged on the 16th of September 2014, you can switch to using:
Organizing our components
Now that we’ve solved everything we had to solve, let’s put all the pieces together!
It really comes down to how we should organize our React components. I guess there’s no perfect answer to that, and certainly not a single one, so here’s the most reasonable solution I could think of. Suggestions are welcome ☺
I think what should be kept in mind, is that react-rails need components to be available on the global scope, but not all of them: only those “root” components that we are going to use with the react_component helper within our views.
Knowing that, here is what I would suggest:
- Create each component in its own module, along with its direct children components that couldn’t be used anywhere else. Only export the main component though.
- Use require to pull external components in a component’s module when they’re needed as children.
You would otherwise ask Sprockets to copy in each and every of your components individually, unnecessarily duplicating a lot of code, as browserify would in turn bundle each component with a copy of its own requirements.
Following those few rules will allow both server- and client-side rendering of your React components, while letting you isolate your components code and organize them at will.
You can find an example project built along the lines of this article on my Github account: https://github.com/olance/rails-react-browserify-example
The commits will show you the steps taken to set up the Rails app from scratch!