diff --git a/examples/styled-components/src/App.js b/examples/styled-components/src/App.js index 74d24a343..732b15fdc 100644 --- a/examples/styled-components/src/App.js +++ b/examples/styled-components/src/App.js @@ -41,6 +41,9 @@ const Context = React.createContext(); const Hook = () => { const [state, setState] = React.useState({ x: 4 }); + + React.useState(0); + React.useEffect(() => { console.log('mount effected 1'); setState(state => ({ diff --git a/src/babel.prod.js b/src/babel.prod.js index cdfa8587a..d0f92ce24 100644 --- a/src/babel.prod.js +++ b/src/babel.prod.js @@ -74,6 +74,7 @@ export default function plugin() { // ensure that this is `hot` from RHL isImportedFromRHL(path, specifier.local) && path.parent.type === 'CallExpression' && + path.parent.arguments.length === 1 && path.parent.arguments[0] && path.parent.arguments[0].type === 'Identifier' ) { diff --git a/src/internal/getReactStack.js b/src/internal/getReactStack.js index 71819a7ab..35d4fdd7b 100644 --- a/src/internal/getReactStack.js +++ b/src/internal/getReactStack.js @@ -33,19 +33,13 @@ const markUpdate = ({ fiber }) => { } const mostResentType = resolveType(fiber.type) || fiber.type; - if (fiber.elementType === fiber.type) { - fiber.elementType = mostResentType; - } fiber.type = mostResentType; + // do not change fiber.elementType to keep old information for the hot-update fiber.expirationTime = 1; if (fiber.alternate) { fiber.alternate.expirationTime = 1; fiber.alternate.type = fiber.type; - // elementType might not exists in older react versions - if ('elementType' in fiber.alternate) { - fiber.alternate.elementType = fiber.elementType; - } } if (fiber.memoizedProps && typeof fiber.memoizedProps === 'object') { diff --git a/src/webpack/patch.js b/src/webpack/patch.js index f83d33e09..ee5624dc5 100644 --- a/src/webpack/patch.js +++ b/src/webpack/patch.js @@ -26,12 +26,13 @@ const injectionStart = { }; const additional = { + '16.10-update': [ - '( // Keep this check inline so it only runs on the false path:\n isCompatibleFamilyForHotReloading(current$$1, element)))', + 'current$$1.elementType === element.type || ( // Keep this check inline so it only runs on the false path:\n isCompatibleFamilyForHotReloading(current$$1, element)))', '(hotCompareElements(current$$1.elementType, element.type, hotUpdateChild(current$$1), current$$1.type)))' ], '16.9-update': [ - '(\n // Keep this check inline so it only runs on the false path:\n isCompatibleFamilyForHotReloading(current$$1, element)))', + 'current$$1.elementType === element.type || (\n // Keep this check inline so it only runs on the false path:\n isCompatibleFamilyForHotReloading(current$$1, element)))', '(hotCompareElements(current$$1.elementType, element.type, hotUpdateChild(current$$1), current$$1.type)))' ], '16.6-update': [ diff --git a/test/__babel_fixtures__/drop-hot-half.prod.js b/test/__babel_fixtures__/drop-hot-half.prod.js new file mode 100644 index 000000000..44b231fb3 --- /dev/null +++ b/test/__babel_fixtures__/drop-hot-half.prod.js @@ -0,0 +1,23 @@ +import { hot } from 'react-hot-loader'; +import { hot as rootHot } from 'react-hot-loader/root'; + +const control = compose( + withDebug, + withDebug, +)(App); + +const targetCase1 = compose( + withDebug, + withDebug, + hot(module), +)(App); + +const targetCase2 = compose( + withDebug, + withDebug, + rootHot, +)(App); + +const removeHot1 = hot(control); +const removeHot2 = hot(module)(control); +const removeHot3 = rootHot(control); \ No newline at end of file diff --git a/test/__snapshots__/babel.test.js.snap b/test/__snapshots__/babel.test.js.snap index 1bfd1531d..ec039c205 100644 --- a/test/__snapshots__/babel.test.js.snap +++ b/test/__snapshots__/babel.test.js.snap @@ -1069,6 +1069,24 @@ exports.default = _default; })();" `; +exports[`babel Targetting "es2015" tags potential React components drop hot half.prod.js 1`] = ` +"'use strict'; + +var _reactHotLoader = require('react-hot-loader'); + +var _root = require('react-hot-loader/root'); + +var control = compose(withDebug, withDebug)(App); + +var targetCase1 = compose(withDebug, withDebug, (0, _reactHotLoader.hot)(module))(App); + +var targetCase2 = compose(withDebug, withDebug, _root.hot)(App); + +var removeHot1 = (0, _reactHotLoader.hot)(control); +var removeHot2 = control; +var removeHot3 = control;" +`; + exports[`babel Targetting "es2015" tags potential React components drop hot.prod.js 1`] = ` "'use strict'; @@ -2179,6 +2197,24 @@ exports.default = _default; })();" `; +exports[`babel Targetting "modern" tags potential React components drop hot half.prod.js 1`] = ` +"'use strict'; + +var _reactHotLoader = require('react-hot-loader'); + +var _root = require('react-hot-loader/root'); + +const control = compose(withDebug, withDebug)(App); + +const targetCase1 = compose(withDebug, withDebug, (0, _reactHotLoader.hot)(module))(App); + +const targetCase2 = compose(withDebug, withDebug, _root.hot)(App); + +const removeHot1 = (0, _reactHotLoader.hot)(control); +const removeHot2 = control; +const removeHot3 = control;" +`; + exports[`babel Targetting "modern" tags potential React components drop hot.prod.js 1`] = ` "'use strict'; diff --git a/test/hot/react-dom.integration.spec.js b/test/hot/react-dom.integration.spec.js index f574475c1..6b4bb2a57 100644 --- a/test/hot/react-dom.integration.spec.js +++ b/test/hot/react-dom.integration.spec.js @@ -5,9 +5,11 @@ import React, { Suspense } from 'react'; import ReactDom from 'react-dom'; // import TestRenderer from 'react-test-renderer'; import ReactHotLoader, { setConfig } from '../../src/index.dev'; -import { configureGeneration, incrementHotGeneration } from '../../src/global/generation'; +import { configureGeneration, enterHotUpdate, incrementHotGeneration } from '../../src/global/generation'; import configuration from '../../src/configuration'; -import { AppContainer } from '../../index'; +import AppContainer from '../../src/AppContainer.dev'; +import reactHotLoader from '../../src/reactHotLoader'; +import reconcileHotReplacement from '../../src/reconciler'; jest.mock('react-dom', () => { const reactDom = require('./react-dom'); @@ -21,6 +23,7 @@ describe(`🔥-dom`, () => { ignoreSFC: true, }); configureGeneration(1, 1); + reactHotLoader.register(AppContainer, 'AppContainer', 'test'); }); const tick = () => new Promise(resolve => setTimeout(resolve, 10)); @@ -213,6 +216,92 @@ describe(`🔥-dom`, () => { expect(mount).toHaveBeenCalledWith('test2'); }); + it('should fail on hook order change', async () => { + const Fun1 = () => { + const [state, setState] = React.useState('test0'); + React.useEffect(() => setState('test1'), []); + return state; + }; + + const el = document.createElement('div'); + ReactDom.render(, el); + + expect(el.innerHTML).toEqual('test0'); + + incrementHotGeneration(); + { + const Fun1 = () => { + React.useState('anotherstate'); + const [state, setState] = React.useState('test0'); + React.useEffect(() => setState('test1'), []); + return state; + }; + expect(() => ReactDom.render(, el)).toThrow(); + } + }); + + it('should set on hook order change if signature provided', async () => { + const ref = React.createRef(); + const App = ({ children }) => ( + + {children} + + ); + const Fun1 = () => { + const [state, setState] = React.useState('test0'); + React.useEffect(() => setState('test1'), []); + return state; + }; + + const Fun2 = () => { + const [state, setState] = React.useState('step1'); + React.useEffect(() => setState('step2'), []); + return state; + }; + + reactHotLoader.signature(Fun1, 'fun1-key1'); + reactHotLoader.register(Fun1, 'Fun1', 'test'); + + const el = document.createElement('div'); + ReactDom.render( + + + + , + el, + ); + + expect(el.innerHTML).toEqual('test0step1'); + await tick(); + expect(el.innerHTML).toEqual('test1step2'); + + { + const Fun1 = () => { + React.useState('anotherstate'); + const [state, setState] = React.useState('test-new'); + React.useEffect(() => setState('test1'), []); + return state; + }; + reactHotLoader.signature(Fun1, 'fun1-key2'); + reactHotLoader.register(Fun1, 'Fun1', 'test'); + + incrementHotGeneration(); + enterHotUpdate(); + reconcileHotReplacement(ref.current); + + expect(() => + ReactDom.render( + + + + , + el, + ), + ).not.toThrow(); + expect(el.innerHTML).toEqual('test-newstep2'); + } + }); + it('should reset hook comparator', async () => { const Fun1 = () => { const [state, setState] = React.useState('test0');