diff --git a/packages/react-hot-loader/src/global/modules.js b/packages/react-hot-loader/src/global/modules.js index d35237272..cb0a0549a 100644 --- a/packages/react-hot-loader/src/global/modules.js +++ b/packages/react-hot-loader/src/global/modules.js @@ -2,6 +2,17 @@ import logger from '../logger' const openedModules = {} +const hotModules = {} + +const createHotModule = () => ({ instances: [], updateTimeout: 0 }) + +export const hotModule = moduleId => { + if (!hotModules[moduleId]) { + hotModules[moduleId] = createHotModule() + } + return hotModules[moduleId] +} + export const isOpened = sourceModule => sourceModule && !!openedModules[sourceModule.id] diff --git a/packages/react-hot-loader/src/hot.dev.js b/packages/react-hot-loader/src/hot.dev.js index 7e267b46b..329e85896 100644 --- a/packages/react-hot-loader/src/hot.dev.js +++ b/packages/react-hot-loader/src/hot.dev.js @@ -3,7 +3,7 @@ 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 } from './global/modules' +import { isOpened as isModuleOpened, hotModule } from './global/modules' import logger from './logger' /* eslint-disable camelcase, no-undef */ @@ -19,18 +19,19 @@ const createHoc = (SourceComponent, TargetComponent) => { return TargetComponent } -const makeHotExport = (sourceModule, getInstances) => { - const updateInstances = () => - setTimeout(() => { - if (sourceModule.id) { - try { - requireIndirect(sourceModule.id) - } catch (e) { - // just swallow - } +const makeHotExport = sourceModule => { + const updateInstances = () => { + const module = hotModule(sourceModule.id) + clearTimeout(module.updateTimeout) + module.updateTimeout = setTimeout(() => { + try { + requireIndirect(sourceModule.id) + } catch (e) { + // just swallow } - getInstances().forEach(inst => inst.forceUpdate()) + module.instances.forEach(inst => inst.forceUpdate()) }) + } if (sourceModule.hot) { // Mark as self-accepted for Webpack @@ -51,8 +52,16 @@ const makeHotExport = (sourceModule, getInstances) => { } const hot = sourceModule => { - let instances = [] - makeHotExport(sourceModule, () => instances) + if (!sourceModule || !sourceModule.id) { + // this is fatal + throw new Error( + 'React-hot-loader: `hot` could not found the `id` property in the `module` you have provided', + ) + } + const moduleId = sourceModule.id + const module = hotModule(moduleId) + makeHotExport(sourceModule) + // TODO: Ensure that all exports from this file are react components. return WrappedComponent => { @@ -60,29 +69,27 @@ const hot = sourceModule => { reactHotLoader.register( WrappedComponent, getComponentDisplayName(WrappedComponent), - `RHL${sourceModule.id}`, + `RHL${moduleId}`, ) return createHoc( WrappedComponent, class ExportedComponent extends Component { componentWillMount() { - instances.push(this) + module.instances.push(this) } componentWillUnmount() { if (isModuleOpened(sourceModule)) { const componentName = getComponentDisplayName(WrappedComponent) logger.error( - `React-hot-loader: Detected AppContainer unmount on module '${ - sourceModule.id - }' update.\n` + + `React-hot-loader: Detected AppContainer unmount on module '${moduleId}' update.\n` + `Did you use "hot(${componentName})" and "ReactDOM.render()" in the same file?\n` + `"hot(${componentName})" shall only be used as export.\n` + `Please refer to "Getting Started" (https://github.com/gaearon/react-hot-loader/).`, ) } - instances = instances.filter(a => a !== this) + module.instances = module.instances.filter(a => a !== this) } render() { diff --git a/packages/react-hot-loader/test/hot.dev.test.js b/packages/react-hot-loader/test/hot.dev.test.js index 45fb77979..7bc8a2452 100644 --- a/packages/react-hot-loader/test/hot.dev.test.js +++ b/packages/react-hot-loader/test/hot.dev.test.js @@ -1,9 +1,10 @@ -import React from 'react' +import React, { Component } from 'react' import { mount } from 'enzyme' import { enter as enterModule, leave as leaveModule, isOpened, + hotModule, } from '../src/global/modules' import hot from '../src/hot.dev' import logger from '../src/logger' @@ -34,6 +35,7 @@ describe('hot (dev)', () => { } hot(sourceModule) + expect(hotModule(sourceModule.id).instances.length).toBe(0) expect(sourceModule.hot.accept).toHaveBeenCalledTimes(1) expect(sourceModule.hot.addStatusHandler).toHaveBeenCalledTimes(1) }) @@ -61,19 +63,61 @@ describe('hot (dev)', () => { wrapper.unmount() }) + it('should redraw component on HRM', done => { + const callbacks = [] + const sourceModule = { + id: 'error42', + hot: { + accept(callback) { + callbacks.push(callback) + }, + }, + } + const spy = jest.fn() + enterModule(sourceModule) + + class MyComponent extends Component { + render() { + spy() + return
42
+ } + } + + const HotComponent = hot(sourceModule)(MyComponent) + hot(sourceModule)(MyComponent) + mount() + expect(spy).toHaveBeenCalledTimes(1) + + expect(callbacks.length).toBe(2) + callbacks.forEach(cb => cb()) + expect(spy).toHaveBeenCalledTimes(1) + setTimeout(() => { + expect(spy).toHaveBeenCalledTimes(3) + done() + }, 1) + }) + it('should trigger error in unmount in opened state', () => { - const sourceModule = { id: 'error42' } + const sourceModule = { id: 'error42_unmount' } enterModule(sourceModule) const Component = () =>
123
const HotComponent = hot(sourceModule)(Component) - const wrapper = mount() - wrapper.unmount() + expect(hotModule(sourceModule.id).instances.length).toBe(0) + const wrapper1 = mount() + const wrapper2 = mount() + expect(hotModule(sourceModule.id).instances.length).toBe(2) + wrapper1.unmount() + expect(hotModule(sourceModule.id).instances.length).toBe(1) + expect(logger.error).toHaveBeenCalledTimes(1) expect(logger.error) - .toHaveBeenCalledWith(`React-hot-loader: Detected AppContainer unmount on module 'error42' update. + .toHaveBeenCalledWith(`React-hot-loader: Detected AppContainer unmount on module 'error42_unmount' update. Did you use "hot(Component)" and "ReactDOM.render()" in the same file? "hot(Component)" shall only be used as export. Please refer to "Getting Started" (https://github.com/gaearon/react-hot-loader/).`) + + wrapper2.unmount() + expect(hotModule(sourceModule.id).instances.length).toBe(0) }) it('it should track module state', () => {