Skip to content

Commit

Permalink
Merge pull request #1338 from gaearon/4.12.13
Browse files Browse the repository at this point in the history
4.12.13
  • Loading branch information
theKashey committed Sep 12, 2019
2 parents 941b41c + 09281ef commit 69b9d48
Show file tree
Hide file tree
Showing 11 changed files with 100 additions and 18 deletions.
9 changes: 6 additions & 3 deletions src/global/generation.js
Expand Up @@ -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
Expand All @@ -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);

Expand All @@ -48,6 +51,7 @@ const decrementHot = () => {
export const configureGeneration = (counter, runs) => {
hotComparisonCounter = counter;
hotComparisonRuns = runs;
hotReplacementGeneration = runs;
};

// TODO: shall it be called from incrementHotGeneration?
Expand All @@ -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;
24 changes: 21 additions & 3 deletions src/hot.dev.js
@@ -1,11 +1,13 @@
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';
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;
Expand All @@ -29,22 +31,38 @@ 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) {
console.error(possibleError);
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());
});
});
};

Expand Down
3 changes: 2 additions & 1 deletion src/internal/getReactStack.js
Expand Up @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions src/proxy/createClassProxy.js
Expand Up @@ -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];
Expand All @@ -412,8 +412,8 @@ function createClassProxy(InitialComponent, proxyKey, options = {}) {
lastInstance,
injectedMembers,
);
getElementComparisonHook(ProxyComponent);
}
getElementComparisonHook(ProxyComponent);
};

// Was constructed once
Expand Down
9 changes: 9 additions & 0 deletions src/proxy/utils.js
Expand Up @@ -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)
Expand Down
12 changes: 9 additions & 3 deletions src/reconciler/proxyAdapter.js
Expand Up @@ -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];
Expand Down
8 changes: 7 additions & 1 deletion src/reconciler/resolver.js
Expand Up @@ -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) {
Expand Down
21 changes: 21 additions & 0 deletions 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;
};
7 changes: 5 additions & 2 deletions test/AppContainer.dev.test.js
Expand Up @@ -347,11 +347,10 @@ describe(`AppContainer (dev)`, () => {

const wrapper = mount(<Indirect App={App} />);
expect(wrapper.text()).toBe('works before');
expect(<App />.type.prototype.render).not.toBe(App.prototype.render);
closeGeneration();
expect(<App />.type.prototype.render).toBe(App.prototype.render);
const originalRender = App.prototype.render;

let newRender;
{
/* eslint-disable */
class SubApp extends Component {
Expand Down Expand Up @@ -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(<App />.type.prototype.render).not.toBe(originalRender);
expect(<App />.type.prototype.render).not.toBe(newRender);
closeGeneration();
expect(<App />.type.prototype.render).toBe(newRender);

configuration.pureRender = pureRender;
});
Expand Down
6 changes: 4 additions & 2 deletions test/proxy/lifecycle-method.test.js
Expand Up @@ -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);

Expand All @@ -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);

Expand Down
15 changes: 14 additions & 1 deletion test/reconciler.test.js
Expand Up @@ -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';
Expand Down Expand Up @@ -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');

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -351,12 +360,16 @@ describe('reconciler', () => {
const wrapper = mount(<App />);
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)

Expand Down

0 comments on commit 69b9d48

Please sign in to comment.