Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?
Rails + Browserify + React + es7

1. Gemfile

gem 'browserify-rails', '1.5.0' # until fix: https://github.com/browserify-rails/browserify-rails/issues/101
gem 'react-rails'

Browserify-rails allows to use browserify within assets pipeline. React-rails is here only to allow to use #react_component (and thus, prerendering).

Note that jquery-rails can be removed from Gemfile, the npm version of jquery and jquery-ujs will be used instead.

2. package.json

Here is a typical package json for es7 + react + rails:

Versions may need update. npm-check is killing it for that.

{
  "name": "my app",
  "dependencies": {
    "babel-plugin-syntax-async-functions": "^6.3.13",
    "babel-plugin-transform-regenerator": "^6.3.18",
    "babel-polyfill": "^6.3.14",
    "babel-preset-es2015": "^6.3.13",
    "babel-preset-react": "^6.3.13",
    "babel-preset-stage-0": "^6.3.13",
    "babelify": "^7.2.0",
    "browserify": "^12.0.1",
    "browserify-incremental": "^3.0.1",
    "es6-promise": "^3.0.2",
    "fetch": "^0.3.6",
    "jquery": "^2.1.4",
    "jquery-ujs": "^1.0.4",
    "react": "^0.14.3",
  },
  "license": "MIT",
  "engines": {
    "node": ">= 0.10"
  },
  "devDependencies": {
    "babelify": "^7.2.0"
  }
}

The fetch plugin is a polyfill that allows to use fetch in any browser.

To add a js plugin in your application, simply add a line in the dependencies object and run npm-check -u (or npm install if you don't like sexyness).

3. config/application.rb

Add at the end of Application class:

config.browserify_rails.commandline_options = "-t [ babelify --presets [ es2015 react stage-0 ] --plugins [ syntax-async-functions transform-regenerator ] ]"
  • es2015 and stage-0 babel presets allow to parse cutting edge es7
  • react babel preset allows to parse jsx
  • syntax-async-function and transform-regenerator allow to use es7 async functions

4. app/assets/javascripts/application.js

//= require_self
//= require react-server
//= require react_ujs

window.$ = window.jQuery = global.$ = require('jquery');
var React = window.React = global.React = require('react');
require( 'jquery-ujs' );
require( 'fetch' );
require( './components' );

Sprockets is only used to require current script and helpers from react-rails. All other files will be imported using browserify.

5. app/assets/javascripts/components.js

require( 'babel-polyfill' );

global.MyFirstComponent = require( 'components/my_first_component' ).default;
global.MySecondComponent = require( 'components/my_second_component' ).default;

This is the central requiring script, that will load all files in the pipeline. Think of it as this root file in ruby gems that just require all the other files (although, I prefer to only require root components, and let them require further subcomponents they need).

A few remarks:

  • this is in components.js rather than application.js, because this is the file server side prerendering will load. Anything declared in application.js won't be available to prerendering
  • requiring babel-polyfill is mandatory to use es7 async functions
  • for a reason I don't understand, it is not possible to use the es7 import syntax in that root file, so you have to use require(...).default instead

6. write some es7 code \o/

Here is an example:

// app/assets/javascripts/components/hello_world.js

import Title      from 'components/title';
import LoremIpsum from 'components/lorem_ipsum';

var propTypes = {
  name: React.PropTypes.string.isRequired,
};

export default class HelloWorld extends React.Component {
  constructor( props ){
    super( props );
    this.state = { name: this.props.name };
  }

  changeName = async () => {
    let resp = await fetch( '/get_new_name' );
    resp = await resp.json();
    this.setState({ name: resp.name });
  }

  render(){
    return (
      <div onClick={this.changeName}>
        <Title>Hello {this.state.name}!</Title>
        <LoremIpsum />
      </div>
    );
  }
}

HelloWorld.propTypes = propTypes;

Do not forget to require this file in your component.js:

global.HelloWorld = require( 'components/hello_world' ).default;

7. load the component in view

From here, it's just stuff as usual:

.container
  .row
    .col-md-12
      = react_component 'HelloWorld', { name: current_user.name }, prerender: true

You sir is a super star. I wish I could give you a job that pays a million. Thank you. This really works and now Isomorphic/Universal all the way.

skflowne commented Jun 10, 2016

This is really nice, thanks for putting the info out there.

bdelmas commented Jun 22, 2016

We can now delete the specific version of browserify-rails in the Gemfile because the issue #101 has been corrected.

n8rzz commented Aug 23, 2016

Thank you for posting this! I've been looking to put together a workable browserify + es2015 + react solution with a minimal amount of gems.

Correct me if I'm wrong, but I do believe there is a typo in your constructor:

this.state = { name: this.props.name };

should instead be:

this.state = { name: props.name };

Component props are not available on this inside of the constructor.

if you don't use prerender and add turbolinks 5 it only works with full page reload… any idea?

@borisrorsvort I had a similar issue and mine was caused by requiring turbolinks after the react_ujs package.

jcuna commented Oct 18, 2016

This looks great, I was using it with brunch but it was causing too much trouble, however, after I stripped down brunch I'm now getting a "Uncaught ReferenceError: global is not defined" in the lines where I require jquery/react and assign global.$, etc. Anybody has any idea?

If you encounter this lovely error:

addComponentAsRefTo(...): Only a ReactOwner can have refs. You might be adding a ref to a component that was not created inside a component's render method, or you have multiple copies of React loaded.

Try dropping //= require react-server and require the following in app/assets/javascripts/application.js:

var ReactDOM = window.ReactDOM = global.ReactDOM = require('react-dom');
var ReactDOMServer = window.ReactDOMServer = global.ReactDOMServer = require('react-dom/server');

Also include react-dom in your package.json via npm install react-dom --save / yarn add react-dom --save

for a reason I don't understand, it is not possible to use the es7 import syntax in that root file, so you have to use require(...).default instead

Which error message do you get? Maybe this? 'import' and 'export' may appear only with 'sourceType: module'

onebree commented Dec 5, 2016

https://gist.github.com/oelmekki/c78cfc8ed1bba0da8cee#4-appassetsjavascriptsapplicationjs

My original application.js includes bootstrap-sprockets (for the gem bootstrap-sass), and ends with require_tree .

Should I add Bootstrap instead via NPM? And is require_tree . necessary?

idoo commented Dec 29, 2016

@oelmekki does it work only with default export?

egoens commented Dec 29, 2016

If you run into a "ReactDOM is not defined" error in the console, the add the following to the application.js file:

var ReactDOM = window.ReactDOM = global.ReactDOM = require('react-dom');

If you get this error:

Invariant Violation: ReactDOM.render(): Invalid component element. This may be caused by unintentionally loading two independent copies of React.

follow @fastdivision's advice:

Remove

//= require react-server

and add

var ReactDOMServer = window.ReactDOMServer = global.ReactDOMServer = require('react-dom/server');

to your application.js – it works like a charm!

Thanks for the great work on this, @oelmekki!

jefree commented Jan 29, 2017

When I run rake assets:precompile on production mode, I'm getting the next error:

ExecJS::RuntimeError: SyntaxError: Unexpected token: operator (>)

I guess it's because the react components are not being transpiled to from jsx to js.

Any idea ?

This solution ended up being too slow for me in development mode – often taking > 6 s to compile assets for each request. After a few other dead ends, ended up switching to https://github.com/mipearson/webpack-rails for a blazing fast development cycle.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment