Skip to content

Commit

Permalink
Refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
gaearon committed Jul 22, 2014
1 parent f493085 commit bb633d5
Show file tree
Hide file tree
Showing 10 changed files with 223 additions and 117 deletions.
7 changes: 7 additions & 0 deletions .jshintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"node": true,
"globals": {
"window": true,
"Notification": true
}
}
21 changes: 17 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,20 @@ It marries React with Webpack [Hot Module Replacement](http://webpack.github.io/

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

![](http://f.cl.ly/items/0d0P3u2T0f2O163K3m1B/2014-07-14%2014_09_02.gif)

![](http://f.cl.ly/items/3T3u3N1d2U30380Z2k2D/2014-07-14%2014_05_49.gif)

## Installation

`npm install react-hot-loader`

## Demo

### [Real Project Video Demo](https://vimeo.com/100010922)

### Bundled Example

![](http://f.cl.ly/items/0d0P3u2T0f2O163K3m1B/2014-07-14%2014_09_02.gif)

![](http://f.cl.ly/items/3T3u3N1d2U30380Z2k2D/2014-07-14%2014_05_49.gif)

## Usage

[Documentation: Using loaders](http://webpack.github.io/docs/using-loaders.html)
Expand All @@ -36,6 +42,13 @@ You can also specify loader in config before `jsx-loader`:

This will enable hot reload for all JSX files.

### Exceptions

Hot reload is disabled for modules that contain no `React.createClass` calls and/or don't export a valid React class.
For example, in the sample project, `app.jsx` doesn't get live updates because it is assumed to have side-effects.

Several components in one file will work as long as their `displayName`s are different.

### Options

* `notify`: Loader can use desktop Notification API to show notifications when a module has been reloaded, or if it loads with an error. By default, this feature is disabled because it doesn't work well with `webpack-dev-server` iframe mode used in the example. If you don't use `webpack-dev-server`'s iframe mode, you might want to enable notifications. Valid values are `none` (default), `errors` and `all`. If `notify` is `errors` or `all`, module load errors won't cause page refresh.
Expand Down
2 changes: 1 addition & 1 deletion example/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ module.exports = {
},
module: {
loaders: [
{ test: /\.jsx$/, loaders: ['react-hot', 'jsx-loader'] }
{ test: /\.jsx$/, loaders: ['react-hot', 'jsx'] }
]
}
};
39 changes: 11 additions & 28 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,37 +12,20 @@ module.exports.pitch = function (remainingRequest) {
query.notify = query.notify || 'none';

return [
'var React = require("react")',
'var notify = require(' + JSON.stringify(require.resolve('./notify')) + ')(' + JSON.stringify(query.notify) + ');',
'var hots = {};',
'function hotCreateClass(spec) {',
' var displayName = spec.displayName;',
' if (hots[displayName]) {',
' console.warn("Found duplicate displayName: " + displayName + " in ' + originalFilename + '.\\n" +',
' "react-hot-loader uses displayName to distinguish between several components in the same file.");',
' }',
' var hot = require(' + JSON.stringify(require.resolve('./hot')) + ')(React);',
' hots[displayName] = hot;',
' return hot.createClass(spec);',
'}',
'function hotUpdateClass(spec) {',
' var displayName = spec.displayName;',
' if (!hots[displayName]) {',
' return hotCreateClass(spec);',
' }',
' return hots[spec.displayName].createClass(spec);',
'}',
'module.exports = require(' + JSON.stringify(patchedModuleRequest) + ')(hotCreateClass);',
'if (module.hot && Object.keys(hots).length > 0) {',
'var React = require("react");',
'var notifier = require(' + JSON.stringify(require.resolve('./makeNotifier')) + ')(' + JSON.stringify(originalFilename) + ', ' + JSON.stringify(query.notify) + ');',
'var moduleUpdater = require(' + JSON.stringify(require.resolve('./makeModuleUpdater')) + ')(' + JSON.stringify(originalFilename) + ', React);',

'module.exports = require(' + JSON.stringify(patchedModuleRequest) + ')(moduleUpdater.createClass);',

'if (module.hot && moduleUpdater.canUpdateModule() && React.isValidClass(module.exports)) {',
' module.hot.accept(' + JSON.stringify(patchedModuleRequest) + ', function () {',
' try {',
' require(' + JSON.stringify(patchedModuleRequest) + ')(hotUpdateClass);',
' Object.keys(hots).forEach(function (key) {',
' hots[key].updateMountedInstances();',
' });',
' notify.success(' + JSON.stringify(originalFilename) + ')',
' require(' + JSON.stringify(patchedModuleRequest) + ')(moduleUpdater.updateClass);',
' moduleUpdater.updateMountedInstances();',
' notifier.handleSuccess()',
' } catch (err) {',
' notify.failure(' + JSON.stringify(originalFilename) + ', err)',
' notifier.handleFailure(err)',
' }',
' });',
'}'
Expand Down
77 changes: 62 additions & 15 deletions hot.js → makeComponentUpdater.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
'use strict';

/**
* Provides `createClass` and `updateClass` which can be used to create and
* later patch a single component with a new version of itself.
*/
module.exports = function (React) {
var mounted = [];
var Mixin = {

/**
* Keeps track of mounted instances.
*/
var TrackInstancesMixin = {
componentDidMount: function () {
mounted.push(this);
},
Expand All @@ -12,6 +20,11 @@ module.exports = function (React) {
}
};


/**
* Establishes a prototype as the "source of truth" and updates its methods on
* subsequent invocations, also patching fresh prototypes to pass calls to it.
*/
var assimilatePrototype = (function () {
var storedPrototype,
knownPrototypes = [];
Expand Down Expand Up @@ -65,14 +78,24 @@ module.exports = function (React) {
};
})();


/**
* Mixes instance tracking into the spec, lets React produce a fresh version
* of the component and assimilates its changes into the old version.
*/
function injectMixinAndAssimilatePrototype(spec) {
spec.mixins = spec.mixins || [];
spec.mixins.push(Mixin);
spec.mixins.push(TrackInstancesMixin);
var Component = React.createClass(spec);
assimilatePrototype(Component.type.prototype);
return Component;
}


/**
* Updates a React component recursively, so even if children define funky
* `shouldComponentUpdate`, they are forced to re-render.
*/
function forceUpdateTree(instance) {
if (instance.forceUpdate) {
instance.forceUpdate();
Expand All @@ -87,22 +110,46 @@ module.exports = function (React) {
}
}


var Component;
return {
createClass: function (spec) {
var FreshComponent = injectMixinAndAssimilatePrototype(spec);
if (!Component) {
Component = FreshComponent;
}

return Component;
},
/**
* Proxies React.createClass to enable hot updates.
*/
function createClass(spec) {
if (Component) {
throw new Error('createClass may only be called once for a given updater.');
}

updateMountedInstances: function () {
mounted.forEach(function (instance) {
instance._bindAutoBindMethods();
forceUpdateTree(instance);
});
Component = injectMixinAndAssimilatePrototype(spec);
return Component;
}

/**
* Proxies React.createClass to apply hot update.
*/
function updateClass(spec) {
if (!Component) {
throw new Error('updateClass may only be called after createClass.');
}

injectMixinAndAssimilatePrototype(spec);
return Component;
}

/**
* Re-binds methods of mounted instances and re-renders them.
*/
function updateMountedInstances() {
mounted.forEach(function (instance) {
instance._bindAutoBindMethods();
forceUpdateTree(instance);
});
}

return {
createClass: createClass,
updateClass: updateClass,
updateMountedInstances: updateMountedInstances
};
};
53 changes: 53 additions & 0 deletions makeModuleUpdater.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
'use strict';

/**
* Provides `createClass` and `updateClass` which can be used as drop-in
* replacement for `React.createClass` in a module. If multiple components
* are defined in the same module, assumes their `displayName`s are different.
*/
module.exports = function (filename, React) {
var componentUpdaters = {};

function createClass(spec) {
var displayName = spec.displayName,
componentUpdater;

if (componentUpdaters[displayName]) {
throw new Error(
'Found duplicate displayName in ' + filename + ': "' + displayName + '".\n' +
'react-hot-loader uses displayName to distinguish between several components in one file.'
);
}

componentUpdater = require('./makeComponentUpdater')(React);
componentUpdaters[displayName] = componentUpdater;

return componentUpdater.createClass(spec);
}

function updateClass(spec) {
var displayName = spec.displayName,
componentUpdater = componentUpdaters[displayName];

return componentUpdater ?
componentUpdater.updateClass(spec) :
createClass(spec);
}

function updateMountedInstances() {
Object.keys(componentUpdaters).forEach(function (displayName) {
componentUpdaters[displayName].updateMountedInstances();
});
}

function canUpdateModule() {
return Object.keys(componentUpdaters).length > 0;
}

return {
createClass: createClass,
updateClass: updateClass,
updateMountedInstances: updateMountedInstances,
canUpdateModule: canUpdateModule
};
};
69 changes: 69 additions & 0 deletions makeNotifier.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit bb633d5

Please sign in to comment.