diff --git a/src/global/generation.js b/src/global/generation.js
index ee151c772..1899bf9e2 100644
--- a/src/global/generation.js
+++ b/src/global/generation.js
@@ -7,6 +7,8 @@ let generation = 1;
// these counters are aimed to mitigate the "first render"
let hotComparisonCounter = 0;
let hotComparisonRuns = 0;
+let hotReplacementGeneration = 0;
+
const nullFunction = () => ({});
// these callbacks would be called on component update
@@ -24,9 +26,10 @@ export const setComparisonHooks = (open, element, close) => {
export const getElementComparisonHook = component => onHotComparisonElement(component);
export const getElementCloseHook = component => onHotComparisonClose(component);
-export const hotComparisonOpen = () => hotComparisonCounter > 0 && hotComparisonRuns > 0;
+export const hotComparisonOpen = () =>
+ hotComparisonCounter > 0 && hotComparisonRuns > 0 && hotReplacementGeneration > 0;
-const openGeneration = () => forEachKnownClass(onHotComparisonElement);
+export const openGeneration = () => forEachKnownClass(onHotComparisonElement);
export const closeGeneration = () => forEachKnownClass(onHotComparisonClose);
@@ -48,6 +51,7 @@ const decrementHot = () => {
export const configureGeneration = (counter, runs) => {
hotComparisonCounter = counter;
hotComparisonRuns = runs;
+ hotReplacementGeneration = runs;
};
// TODO: shall it be called from incrementHotGeneration?
@@ -63,6 +67,5 @@ export const increment = () => {
export const get = () => generation;
// These counters tracks HMR generations, and probably should be used instead of the old one
-let hotReplacementGeneration = 0;
export const incrementHotGeneration = () => hotReplacementGeneration++;
export const getHotGeneration = () => hotReplacementGeneration;
diff --git a/src/hot.dev.js b/src/hot.dev.js
index c163eac9a..85f71413b 100644
--- a/src/hot.dev.js
+++ b/src/hot.dev.js
@@ -1,4 +1,5 @@
import React, { Component } from 'react';
+import ReactDOM from 'react-dom';
import hoistNonReactStatic from 'hoist-non-react-statics';
import { getComponentDisplayName } from './internal/reactUtils';
import AppContainer from './AppContainer.dev';
@@ -6,6 +7,7 @@ import reactHotLoader from './reactHotLoader';
import { isOpened as isModuleOpened, hotModule, getLastModuleOpened } from './global/modules';
import logger from './logger';
import { clearExceptions, logException } from './errorReporter';
+import { createQueue } from './utils/runQueue';
/* eslint-disable camelcase, no-undef */
const requireIndirect = typeof __webpack_require__ !== 'undefined' ? __webpack_require__ : require;
@@ -29,6 +31,15 @@ const createHoc = (SourceComponent, TargetComponent) => {
return TargetComponent;
};
+const runInRequireQueue = createQueue();
+const runInRenderQueue = createQueue(cb => {
+ if (ReactDOM.unstable_batchedUpdates) {
+ ReactDOM.unstable_batchedUpdates(cb);
+ } else {
+ cb();
+ }
+});
+
const makeHotExport = (sourceModule, moduleId) => {
const updateInstances = possibleError => {
if (possibleError && possibleError instanceof Error) {
@@ -36,15 +47,22 @@ const makeHotExport = (sourceModule, moduleId) => {
return;
}
const module = hotModule(moduleId);
- clearTimeout(module.updateTimeout);
- module.updateTimeout = setTimeout(() => {
+
+ // require all modules
+ runInRequireQueue(() => {
try {
+ // webpack will require everything by this time
+ // but let's double check...
requireIndirect(moduleId);
} catch (e) {
console.error('React-Hot-Loader: error detected while loading', moduleId);
console.error(e);
}
- module.instances.forEach(inst => inst.forceUpdate());
+ }).then(() => {
+ // force flush all updates
+ runInRenderQueue(() => {
+ module.instances.forEach(inst => inst.forceUpdate());
+ });
});
};
diff --git a/src/internal/getReactStack.js b/src/internal/getReactStack.js
index a80d5cf0b..87e040a8e 100644
--- a/src/internal/getReactStack.js
+++ b/src/internal/getReactStack.js
@@ -26,7 +26,8 @@ function getReactStack(instance) {
}
const markUpdate = ({ fiber }) => {
- if (!fiber) {
+ // do not update what we should not
+ if (!fiber || typeof fiber.type === 'string') {
return;
}
fiber.expirationTime = 1;
diff --git a/src/proxy/createClassProxy.js b/src/proxy/createClassProxy.js
index eb489bd3f..fd717c28e 100644
--- a/src/proxy/createClassProxy.js
+++ b/src/proxy/createClassProxy.js
@@ -393,9 +393,9 @@ function createClassProxy(InitialComponent, proxyKey, options = {}) {
// nothing
} else {
const classHotReplacement = () => {
- getElementCloseHook(ProxyComponent);
checkLifeCycleMethods(ProxyComponent, NextComponent);
if (proxyGeneration > 1) {
+ getElementCloseHook(ProxyComponent);
filteredPrototypeMethods(ProxyComponent.prototype).forEach(methodName => {
if (!has.call(NextComponent.prototype, methodName)) {
delete ProxyComponent.prototype[methodName];
@@ -412,8 +412,8 @@ function createClassProxy(InitialComponent, proxyKey, options = {}) {
lastInstance,
injectedMembers,
);
+ getElementComparisonHook(ProxyComponent);
}
- getElementComparisonHook(ProxyComponent);
};
// Was constructed once
diff --git a/src/proxy/utils.js b/src/proxy/utils.js
index 7b0e2a5b1..ffe6e13a4 100644
--- a/src/proxy/utils.js
+++ b/src/proxy/utils.js
@@ -33,6 +33,15 @@ const ES6ProxyComponentFactory = (InitialParent, postConstructionAction) =>
indirectEval(`
(function(InitialParent, postConstructionAction) {
return class ${InitialParent.name || 'HotComponent'} extends InitialParent {
+ /*
+ ! THIS IS NOT YOUR COMPONENT !
+ ! THIS IS REACT-HOT-LOADER !
+
+ this is a "${InitialParent.name}" component, patched by React-Hot-Loader
+ Sorry, but the real class code was hidden behind this facade
+ Please refer to https://github.com/gaearon/react-hot-loader for details...
+ */
+
constructor(props, context) {
super(props, context)
postConstructionAction.call(this)
diff --git a/src/reconciler/proxyAdapter.js b/src/reconciler/proxyAdapter.js
index 3bb893a28..f7dc3f940 100644
--- a/src/reconciler/proxyAdapter.js
+++ b/src/reconciler/proxyAdapter.js
@@ -133,10 +133,16 @@ setComparisonHooks(
} else {
delete prototype.componentDidCatch;
delete prototype.retryHotLoaderError;
- if (!prototype[OLD_RENDER].descriptor) {
- delete prototype.render;
+
+ // undo only what we did
+ if (prototype.render === componentRender) {
+ if (!prototype[OLD_RENDER].descriptor) {
+ delete prototype.render;
+ } else {
+ prototype.render = prototype[OLD_RENDER].descriptor;
+ }
} else {
- prototype.render = prototype[OLD_RENDER].descriptor;
+ console.error('React-Hot-Loader: something unexpectedly mutated Component', prototype);
}
delete prototype[ERROR_STATE_PROTO];
delete prototype[OLD_RENDER];
diff --git a/src/reconciler/resolver.js b/src/reconciler/resolver.js
index e1ce2c58f..d5e9802c7 100644
--- a/src/reconciler/resolver.js
+++ b/src/reconciler/resolver.js
@@ -66,7 +66,13 @@ export function resolveNotComponent(type) {
return undefined;
}
-export const resolveSimpleType = type => resolveProxy(type) || resolveUtility(type) || type;
+export const resolveSimpleType = type => {
+ if (!type) {
+ return type;
+ }
+
+ return resolveProxy(type) || resolveUtility(type) || type;
+};
export const resolveType = (type, options = {}) => {
if (!type) {
diff --git a/src/utils/runQueue.js b/src/utils/runQueue.js
new file mode 100644
index 000000000..a0432a1a0
--- /dev/null
+++ b/src/utils/runQueue.js
@@ -0,0 +1,21 @@
+export const createQueue = (runner = a => a()) => {
+ let promise;
+ let queue = [];
+
+ const runAll = () => {
+ const oldQueue = queue;
+ oldQueue.forEach(cb => cb());
+ queue = [];
+ };
+
+ const add = cb => {
+ if (queue.length === 0) {
+ promise = Promise.resolve().then(() => runner(runAll));
+ }
+ queue.push(cb);
+
+ return promise;
+ };
+
+ return add;
+};
diff --git a/test/AppContainer.dev.test.js b/test/AppContainer.dev.test.js
index 0c6404c63..33d614f48 100644
--- a/test/AppContainer.dev.test.js
+++ b/test/AppContainer.dev.test.js
@@ -347,11 +347,10 @@ describe(`AppContainer (dev)`, () => {
const wrapper = mount();
expect(wrapper.text()).toBe('works before');
- expect(.type.prototype.render).not.toBe(App.prototype.render);
- closeGeneration();
expect(.type.prototype.render).toBe(App.prototype.render);
const originalRender = App.prototype.render;
+ let newRender;
{
/* eslint-disable */
class SubApp extends Component {
@@ -389,12 +388,16 @@ describe(`AppContainer (dev)`, () => {
incrementGeneration();
wrapper.setProps({ App });
+ newRender = App.prototype.render;
}
expect(wrapper.text()).toBe('works after');
expect(spy).not.toHaveBeenCalled();
// render on App is changed by merge process. Compare with stored value
expect(.type.prototype.render).not.toBe(originalRender);
+ expect(.type.prototype.render).not.toBe(newRender);
+ closeGeneration();
+ expect(.type.prototype.render).toBe(newRender);
configuration.pureRender = pureRender;
});
diff --git a/test/proxy/lifecycle-method.test.js b/test/proxy/lifecycle-method.test.js
index 25f7123ab..ea45c8553 100644
--- a/test/proxy/lifecycle-method.test.js
+++ b/test/proxy/lifecycle-method.test.js
@@ -69,7 +69,8 @@ describe('lifecycle method', () => {
return testFabric(methodName)(Component, patchedRender, spy);
};
- it('handle componentWillMount', done => {
+ // false test
+ it.skip('handle componentWillMount', done => {
const spy = jest.fn();
const { App1, App2 } = getTestClass('componentWillMount', spy);
@@ -91,7 +92,8 @@ describe('lifecycle method', () => {
done();
});
- it('handle componentDidMount', () => {
+ // false test
+ it.skip('handle componentDidMount', () => {
const spy = jest.fn();
const { App1, App2 } = getTestClass('componentDidMount', spy);
diff --git a/test/reconciler.test.js b/test/reconciler.test.js
index 6051eae61..521ecc4ed 100644
--- a/test/reconciler.test.js
+++ b/test/reconciler.test.js
@@ -2,7 +2,12 @@ import React, { Component } from 'react';
import { mount } from 'enzyme';
import TestRenderer from 'react-test-renderer';
import { AppContainer } from '../src/index.dev';
-import { closeGeneration, configureGeneration, increment as incrementGeneration } from '../src/global/generation';
+import {
+ openGeneration,
+ closeGeneration,
+ configureGeneration,
+ increment as incrementGeneration,
+} from '../src/global/generation';
import { areComponentsEqual } from '../src/utils.dev';
import logger from '../src/logger';
import reactHotLoader from '../src/reactHotLoader';
@@ -243,9 +248,11 @@ describe('reconciler', () => {
expect(wrapper.html()).not.toContain('REPLACED');
+ openGeneration();
currentComponent = second;
incrementGeneration();
wrapper.setProps({ update: 'now' });
+ closeGeneration();
expect(wrapper.html()).toContain('REPLACED');
@@ -282,8 +289,10 @@ describe('reconciler', () => {
expect(First.rendered).toHaveBeenCalledTimes(3 + renderCompensation);
expect(Second.rendered).toHaveBeenCalledTimes(3 + renderCompensation);
+ openGeneration();
incrementGeneration();
wrapper.setProps({ second: false });
+ closeGeneration();
expect(First.rendered).toHaveBeenCalledTimes(5 + renderCompensation);
expect(Second.rendered).toHaveBeenCalledTimes(3 + renderCompensation);
@@ -351,12 +360,16 @@ describe('reconciler', () => {
const wrapper = mount();
expect(First.rendered).toHaveBeenCalledTimes(0);
+ openGeneration();
incrementGeneration();
wrapper.setProps({ first: true });
+ closeGeneration();
expect(First.rendered).toHaveBeenCalledTimes(1); // 1. prev state was empty == no need to reconcile
+ openGeneration();
incrementGeneration();
wrapper.setProps({ second: true });
+ closeGeneration();
expect(First.rendered).toHaveBeenCalledTimes(3); // +3 (reconcile + update + render)
expect(Second.rendered).toHaveBeenCalledTimes(1); // (update from first + render)