Skip to content

Instantly share code, notes, and snippets.

@oelmekki
Created December 30, 2015 19:37
Show Gist options
  • Save oelmekki/c78cfc8ed1bba0da8cee to your computer and use it in GitHub Desktop.
Save oelmekki/c78cfc8ed1bba0da8cee to your computer and use it in GitHub Desktop.
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
@fastdivision
Copy link

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

@h0jeZvgoxFepBQ2C
Copy link

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
Copy link

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
Copy link

idoo commented Dec 29, 2016

@oelmekki does it work only with default export?

@egoens
Copy link

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');

@c-lliope
Copy link

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
Copy link

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 ?

@c-lliope
Copy link

c-lliope commented Feb 1, 2017

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.

@Tetramputechture
Copy link

As someone who has been struggling to implement a Jest testing framework on a Rails app using the react-rails gem, thank you. I have finally done it, after 4 days, thanks to this tutorial finally getting both my tests and my rendering to work. You are great!

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