Skip to content

Commit

Permalink
Merge pull request renchap#10 from renchap/hmr-build
Browse files Browse the repository at this point in the history
HMR + Build + Style
  • Loading branch information
renchap authored Feb 23, 2017
2 parents ff1b3c5 + 8d78094 commit f6281fc
Show file tree
Hide file tree
Showing 10 changed files with 370 additions and 192 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/javascript/webpacker_react-npm-module/dist/
node_modules/
/.bundle/
/.yardoc
Expand All @@ -10,6 +9,8 @@ node_modules/
/spec/reports/
/tmp/

/javascript/webpacker_react-npm-module/dist/*.js

test/example_app/log/*
test/example_app/tmp/*
test/example_app/public/packs
Expand Down
100 changes: 95 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ This gem is a work in progress. Final feature list:
- [x] uses the new Webpacker way of integrating Javascript with Rails (using packs)
- [x] render React components from views using a `react_component` helper
- [x] render React components from controllers using `render react_component: 'name'`
- [x] support for [hot reloading](https://github.com/gaearon/react-hot-loader)
- [ ] render components server-side
- [ ] support for [hot reloading](https://github.com/gaearon/react-hot-loader)
- [ ] use a Rails generator to create new components

## Installation
Expand Down Expand Up @@ -47,10 +47,10 @@ The first step is to register your root components (those you want to load from
In your pack file (`app/javascript/packs/*.js`), import your components as well as `webpacker-react` and register them. Considering you have a component in `app/javascript/components/hello.js`:

```javascript
import Hello from 'components/hello';
import WebpackerReact from 'webpacker-react';
import Hello from 'components/hello'
import WebpackerReact from 'webpacker-react'

WebpackerReact.register(Hello);
WebpackerReact.register(Hello)
```

Now you can render React components from your views or your controllers.
Expand All @@ -77,6 +77,95 @@ You can pass any of the usual arguments to render in this call: `layout`, `statu

*Note: you need to have [Webpack process your code](https://github.com/rails/webpacker#binstubs) before it is available to the browser, either by manually running `./bin/webpack` or having the `./bin/webpack-watcher` process running.*

### Hot Module Replacement

[HMR](https://webpack.js.org/guides/hmr-react/) allows to reload / add / remove modules live in the browser without
reloading the page. This allows any change you make to your React components to be applied as soon as you save,
preserving their current state.

First, install `react-hot-loader`:

```
./bin/yarn add [email protected]
```

You then need to update your Webpack config.

We provide a convenience function to add the necessary changes to your config if it's not
significantly different than the standard Webpacker config:

```js
//development.js
...

var configureHotModuleReplacement = require('webpacker-react/configure-hot-module-replacement')

var sharedConfig = require('./shared.js')
sharedConfig = configureHotModuleReplacement(sharedConfig)

module.exports = merge(sharedConfig, ...)
```

If you need to change your configuration manually:

1. set the public URL used to load `webpack-dev-server` assets
```js
{
output: {
publicPath: 'http://localhost:8080'
}
}
```

2. add `react-hot-loader/babel` to your `babel-loader` rules:
```javascript
{
module: {
rules: [
{
test: /\.jsx?(.erb)?$/,
exclude: /node_modules/,
loader: 'babel-loader',
options: {
presets: [
'react',
[ 'latest', { 'es2015': { 'modules': false } } ]
],
plugins: ['react-hot-loader/babel']
}
}
}
```

3. prepend `react-hot-loader/patch` to your entries:
```javascript
{
entry:
{ application: [ 'react-hot-loader/patch', '../app/javascript/packs/application.js' ],
...
}
```

4. you now need to use `webpack-dev-server` (in place of `webpack` or `webpack-watcher`). Make sure the following line is in your development.rb:
```ruby
config.x.webpacker[:dev_server_host] = 'http://localhost:8080/'
```
and start `webpack-dev-server` in hot replacement mode:
```
./bin/webpack-dev-server --hot
```

5. finally opt in to HMR from your pack files:
```es6
import SomeRootReactComponent from 'components/some-root-react-component'
import WebpackerReact from 'webpacker-react/hmr'
WebpackerReact.register(SomeRootReactComponent)
if (module.hot)
module.hot.accept('components/some-root-react-component', () =>
WebpackerReact.renderOnHMR(SomeRootReactComponent) )
```

## Development

To work on this gem locally, you first need to clone and setup [the example application](https://github.com/renchap/webpacker-react-example).
Expand All @@ -90,7 +179,8 @@ gem 'webpacker-react', path: '~/code/webpacker-react/'
Finally, you need to tell Yarn to use your local copy of the NPM module in this application, using [`yarn link`](https://yarnpkg.com/en/docs/cli/link):

```
$ cd ~/code/webpacker-react/javascript/webpacker_react-npm-module/
$ cd ~/code/webpacker-react/javascript/webpacker_react-npm-module/dist/
$ yarn # builds the code
$ yarn link
success Registered "webpacker-react".
info You can now run `yarn link "webpacker-react"` in the projects where you want to use this module and it will be used instead.
Expand Down
4 changes: 3 additions & 1 deletion circle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ checkout:
post:
- yarn:
pwd: javascript/webpacker_react-npm-module
- yarn:
pwd: javascript/webpacker_react-npm-module/dist
- yarn link:
pwd: javascript/webpacker_react-npm-module
pwd: javascript/webpacker_react-npm-module/dist
test:
pre:
- test/example_app/bin/yarn
Expand Down
21 changes: 21 additions & 0 deletions javascript/webpacker_react-npm-module/dist/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "webpacker-react",
"version": "0.0.5",
"description": "Javascript",
"main": "index.js",
"homepage": "https://github.com/renchap/webpacker-react",
"repository": "renchap/webpacker-react",
"author": {
"name": "Renaud Chaput",
"email": "[email protected]"
},
"peerDependencies": {
"react": ">= 0.14",
"react-dom": ">= 0.14"
},
"files": ["index.js", "configure-hot-module-replacement.js", "hmr.js"],
"scripts": {
"prepublish": "cd .. ; yarn run build"
},
"license": "MIT"
}
4 changes: 4 additions & 0 deletions javascript/webpacker_react-npm-module/dist/yarn.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1


23 changes: 4 additions & 19 deletions javascript/webpacker_react-npm-module/package.json
Original file line number Diff line number Diff line change
@@ -1,26 +1,11 @@
{
"name": "webpacker-react",
"version": "0.0.5",
"description": "Javascript",
"main": "dist/index.js",
"homepage": "https://github.com/renchap/webpacker-react",
"repository": "renchap/webpacker-react",
"peerDependencies": {
"react": ">= 0.14",
"react-dom": ">= 0.14"
},
"name": "webpacker-react-build",
"private": true,
"scripts": {
"build": "babel src --presets=babel-preset-es2015 --out-dir dist",
"test": "echo \"Error: no test specified\" && exit 1",
"prepublish": "npm run build"
"build": "babel src --presets=babel-preset-es2015 --out-dir dist"
},
"devDependencies": {
"babel-cli": "^6.18.0",
"babel-preset-es2015": "^6.18.0"
},
"author": {
"name": "Renaud Chaput",
"email": "[email protected]"
},
"license": "MIT"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import webpack from 'webpack'
import merge from 'webpack-merge'

function configureHotModuleReplacement(config) {
let webpackDevServerAddr = process.env['WEBPACK_DEV_SERVER_ADDR'] || 'http://localhost:8080/'
config = merge(
config,
{
output: {
publicPath: webpackDevServerAddr // needed for HMR to know where to load the hot update chunks
},
plugins: [
new webpack.NamedModulesPlugin()
]
}
)

config.module.rules = config.module.rules.map( rule => {
if (rule.loader === 'babel-loader') {
return merge(rule, {options: {plugins: ['react-hot-loader/babel']}})
} else {
return rule
}
})

for (let key of Object.keys(config.entry)) {
if (!(config.entry[key] instanceof Array)) {
config.entry[key] = [config.entry[key]]
}
config.entry[key].unshift('react-hot-loader/patch')
}
return config
}

module.exports = configureHotModuleReplacement
9 changes: 9 additions & 0 deletions javascript/webpacker_react-npm-module/src/hmr.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { AppContainer } from 'react-hot-loader'
import WebpackerReact from 'webpacker-react'
import React from 'react'

WebpackerReact.registerWrapForHMR( (reactElement) => {
return React.createElement(AppContainer, {}, reactElement)
})

export default WebpackerReact
83 changes: 57 additions & 26 deletions javascript/webpacker_react-npm-module/src/index.js
Original file line number Diff line number Diff line change
@@ -1,55 +1,86 @@
import React from 'react'
import ReactDOM from 'react-dom'

const CLASS_ATTRIBUTE_NAME = 'data-react-class'
const PROPS_ATTRIBUTE_NAME = 'data-react-props'

var WebpackerReact = {
eventsRegistered: false,
registeredComponents : {},
wrapForHMR: null,

render: function(node, component) {
var propsJson = node.getAttribute(PROPS_ATTRIBUTE_NAME)
var props = propsJson && JSON.parse(propsJson)

let reactElement = React.createElement(component, props)
if (this.wrapForHMR) {
reactElement = this.wrapForHMR(reactElement)
}
ReactDOM.render(reactElement, node)
},

renderOnHMR: function(component) {
var name = component.name
this.registeredComponents[name] = component

if (!this.wrapForHMR) {
console.warn('webpack-react: renderOnHMR called but not elements not wrapped for HMR')
}

var toMount = document.querySelectorAll(`[${CLASS_ATTRIBUTE_NAME}=${name}]`)
for (var i = 0; i < toMount.length; ++i) {
var node = toMount[i]

this.render(node, component)
}
},

registerWrapForHMR(wrapForHMR) {
this.wrapForHMR = wrapForHMR
},

register: function(component) {
var name = component.name;
var name = component.name

if(this.registeredComponents[name]) {
console.warn('webpacker-react: Cant register component, another one with this name is already registered: ' + name);
return false;
if (this.registeredComponents[name]) {
console.warn('webpacker-react: Cant register component, another one with this name is already registered: ' + name)
return false
}

this.registeredComponents[name] = component;
return true;
this.registeredComponents[name] = component
return true
},

addEventEventhandlers: function() {
var registeredComponents = this.registeredComponents;
var registeredComponents = this.registeredComponents

if(this.eventsRegistered == true) {
console.warn("webpacker-react: events have already been registered");
return false;
if (this.eventsRegistered == true) {
console.warn("webpacker-react: events have already been registered")
return false
}

document.addEventListener("DOMContentLoaded", function() {
const CLASS_ATTRIBUTE_NAME = 'data-react-class';
const PROPS_ATTRIBUTE_NAME = 'data-react-props';

document.addEventListener("DOMContentLoaded", () => {
var toMount = document.querySelectorAll('[' + CLASS_ATTRIBUTE_NAME + ']')

for (var i = 0; i < toMount.length; ++i) {
var node = toMount[i];
var className = node.getAttribute(CLASS_ATTRIBUTE_NAME);
var propsJson = node.getAttribute(PROPS_ATTRIBUTE_NAME);
var props = propsJson && JSON.parse(propsJson);

var constructor = registeredComponents[className];
if(constructor) {
ReactDOM.render(React.createElement(constructor, props), node);
var node = toMount[i]
var className = node.getAttribute(CLASS_ATTRIBUTE_NAME)
var component = registeredComponents[className]

if (component) {
this.render(node, component)
} else {
console.error('webpacker-react: cant render a component that has not been registered: ' + className)
}
}
});

this.eventsRegistered = true;
return true;
this.eventsRegistered = true
return true
}
};

WebpackerReact.addEventEventhandlers();
WebpackerReact.addEventEventhandlers()

export default WebpackerReact;
export default WebpackerReact
Loading

0 comments on commit f6281fc

Please sign in to comment.