Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
gaearon committed Jul 13, 2014
0 parents commit cc161c6
Show file tree
Hide file tree
Showing 9 changed files with 255 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules
.DS_Store
46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# react-hot-loader

This is a **highly experimental** proof of concept of [React live code editing](http://www.youtube.com/watch?v=pw4fKkyPPg8).
It marries React with Webpack [Hot Module Replacement](http://webpack.github.io/docs/hot-module-replacement.html) by monkeypatching `React.createClass`.

Inspired by [react-proxy-loader](https://github.com/webpack/react-proxy-loader).

### Running Example

```
npm install
cd example
webpack-dev-server --hot
open http://localhost:8080/webpack-dev-server/bundle
```

Then edit `example/a.jsx` and `example/b.jsx`.
Your changes should be displayed live, without unmounting components or destroying their state.

### Limitations

* You have to include component's displayName in `require` call

### Implementation Notes

Currently, it keeps a list of mounted instances and updates their prototypes when an update comes in.
A better approach may be to make monkeypatch `createClass` to return a proxy object [as suggested by Pete Hunt](https://github.com/webpack/webpack/issues/341#issuecomment-48372300):

## Installation

`npm install react-hot-loader`

## Usage

[Documentation: Using loaders](http://webpack.github.io/docs/using-loaders.html)

```javascript
// Currently you have to pass displayName to require call:
var Button = require('react-hot?Button!./button');
```

When a component is imported that way, changes to its code should be applied **without unmounting it or losing its state**.

# License

MIT (http://www.opensource.org/licenses/mit-license.php)
47 changes: 47 additions & 0 deletions example/a.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/** @jsx React.DOM */

var React = require('react'),
B = require('react-hot?B!./b');

var A = React.createClass({
getInitialState: function () {
return {
number: Math.round(Math.random() * 100)
};
},

render: function() {
return (
<div>
<p>Open this editor, edit and save <code>example/a.jsx</code>.</p>
<p><b>The number should not change.</b></p>

{this.renderStuff()}

<p>This should also work for children:</p>
<B />
</div>
);
},

renderStuff: function () {
return (
<div>
<input type='text' value={this.state.number} />
<button onClick={this.incrementNumber}>Increment by one</button>
</div>
);
},

incrementNumber: function () {
this.setState({
number: this.state.number + 1
});
},

componentWillUnmount: function () {
window.alert('Unmounting parent');
}
});

module.exports = A;
6 changes: 6 additions & 0 deletions example/app.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/** @jsx React.DOM */

var React = require('react'),
A = require('react-hot?A!./a');

React.renderComponent(<A />, document.body);
20 changes: 20 additions & 0 deletions example/b.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/** @jsx React.DOM */

var React = require('react');

var B = React.createClass({
render: function() {
return (
<div style={{background: 'purple',color: 'white'}}>
<p>I am <code>example/b.jsx</code>, feel free to edit me.</p>
<img src='http://facebook.github.io/react/img/logo_og.png' width='200' />
</div>
);
},

componentWillUnmount: function () {
window.alert('Unmounting child');
}
});

module.exports = B;
21 changes: 21 additions & 0 deletions example/webpack.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
var path = require('path');
var webpack = require('webpack');

module.exports = {
entry: ['webpack/hot/dev-server', './app'],
output: {
path: path.join(__dirname, 'output'),
filename: 'bundle.js'
},
resolveLoader: {
modulesDirectories: ['..', 'node_modules']
},
resolve: {
extensions: ['', '.jsx', '.js']
},
module: {
loaders: [
{ test: /\.jsx$/, loader: 'jsx-loader' }
]
}
};
41 changes: 41 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
module.exports = function() {};
module.exports.pitch = function (remainingRequest) {
this.cacheable && this.cacheable();
var displayName = this.query.substring(1);
var moduleRequest = "!!" + remainingRequest;

return [
'var HotUpdateMixin = require(' + JSON.stringify(require.resolve('./makeHotUpdateMixin')) + ')();',
'var React = require("react");',
'function runWithMonkeyPatchedReact(f) {',
' var realCreateClass = React.createClass;',
' var injected = 0;',
' React.createClass = function createHotUpdateClass(spec) {',
' if (spec.displayName === ' + JSON.stringify(displayName) + ') {',
' if (!spec.mixins) spec.mixins = [];',
' spec.mixins.push(HotUpdateMixin.Mixin);',
' injected++;',
' }',
' return realCreateClass(spec);',
' };',
' f();',
' if (injected === 0) {',
' console.warn(\'Could not find component with displayName: ' + JSON.stringify(displayName) + '\');',
' } else if (injected > 1) {',
' console.warn(\'Found more than one component with displayName: ' + JSON.stringify(displayName) + '\');',
' }',
' React.createClass = realCreateClass;',
'}',
'runWithMonkeyPatchedReact(function () {',
' module.exports = require(' + JSON.stringify(moduleRequest) + ');',
'});',
'if (module.hot) {',
' module.hot.accept(' + JSON.stringify(moduleRequest) + ', function() {',
' runWithMonkeyPatchedReact(function () {',
' module.exports = require(' + JSON.stringify(moduleRequest) + ');',
' });',
' HotUpdateMixin.acceptUpdate(module.exports);',
' });',
'}'
].join('\n');
};
42 changes: 42 additions & 0 deletions makeHotUpdateMixin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
var setPrototypeOf = Object.setPrototypeOf || function (obj, proto) {
/* jshint proto:true */
obj.__proto__ = proto;
return obj;
};

module.exports = function () {
var mounted = [];

var Mixin = {
componentDidMount: function () {
mounted.push(this);
},

componentWillUnmount: function () {
mounted.splice(mounted.indexOf(this), 1);
}
};

function forceUpdates() {
mounted.forEach(function (instance) {
instance.forceUpdate();
});
}

function acceptUpdate(FreshComponent) {
var freshProto = FreshComponent.componentConstructor.prototype;

mounted.forEach(function (instance) {
setPrototypeOf(instance, freshProto);
instance.constructor.prototype = freshProto;
instance._bindAutoBindMethods();
});

window.setTimeout(forceUpdates, 0);
}

return {
Mixin: Mixin,
acceptUpdate: acceptUpdate
};
};
30 changes: 30 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "react-hot-loader",
"version": "0.1.0",
"description": "Webpack loader that enables live-editing React components without unmounting or losing their state",
"main": "index.js",
"directories": {
"example": "example"
},
"devDependencies": {
"react": "^0.10.0",
"jsx-loader": "^0.10.2",
"webpack": "^1.3.1"
},
"repository": {
"type": "git",
"url": "https://github.com/gaearon/react-hot-loader.git"
},
"keywords": [
"react",
"webpack",
"hmr",
"livereload"
],
"author": "Dan Abramov",
"license": "MIT",
"bugs": {
"url": "https://github.com/gaearon/react-hot-loader/issues"
},
"homepage": "https://github.com/gaearon/react-hot-loader"
}

0 comments on commit cc161c6

Please sign in to comment.