Skip to content

Commit

Permalink
Merge pull request #818 from gaearon/module-id
Browse files Browse the repository at this point in the history
Bound 'hot' to module.id
  • Loading branch information
theKashey authored Jan 23, 2018
2 parents 181bf59 + e292f99 commit 2f2e01f
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 24 deletions.
11 changes: 11 additions & 0 deletions packages/react-hot-loader/src/global/modules.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand Down
45 changes: 26 additions & 19 deletions packages/react-hot-loader/src/hot.dev.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand All @@ -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
Expand All @@ -51,38 +52,44 @@ 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 => {
// register proxy for wrapped component
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() {
Expand Down
54 changes: 49 additions & 5 deletions packages/react-hot-loader/test/hot.dev.test.js
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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)
})
Expand Down Expand Up @@ -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 <div>42</div>
}
}

const HotComponent = hot(sourceModule)(MyComponent)
hot(sourceModule)(MyComponent)
mount(<HotComponent />)
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 = () => <div>123</div>
const HotComponent = hot(sourceModule)(Component)
const wrapper = mount(<HotComponent />)
wrapper.unmount()
expect(hotModule(sourceModule.id).instances.length).toBe(0)
const wrapper1 = mount(<HotComponent />)
const wrapper2 = mount(<HotComponent />)
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', () => {
Expand Down

0 comments on commit 2f2e01f

Please sign in to comment.