Repository: gaearon/react-hot-loader
Pull request #608 (2017-07-15 13:13:19)

To catch a remount: Add debug information
The main goal of Hot Reloading - is to apply code changes on a live application.
But, If you can NOT preserve application state, including internal component state and component life cycle - it is better NOT to use Hot Loading features at all.

Unfortunately, It is not easy to make this feature work for any complex application - any @decorator, functional composition chain or too complex HoC - and everything will break.

This PR will solve this issue. It will add debug information, so a developer will understand which component is not patched and is causing local remount.

There are 3 types of components:
1. Registered ones. Work as they always do. Exists in `idsByType`.
2. Ones from node_modules. Work as they always do. Exists in `hasCreatedElementsByType`
3. Unregistered hot-reloader. Will be added by `signature` to `knownSignatures` for the first time, and reported as an issue for the second. But only after HMR event.

<img width="1004" alt="screen shot 2017-07-15 at 11 09 17 pm" src="https://user-images.githubusercontent.com/582410/28239586-d499169e-69b2-11e7-89c0-7f5abef75b23.png">

HRM event will be caught by re-registration of any normal component. In this case some `different` components with the same signature (styled-components for example) will not soil console during startup.


Patch:
diff --git a/src/patch.dev.js b/src/patch.dev.js
index dbd334b..f44b374 100644
--- a/src/patch.dev.js
+++ b/src/patch.dev.js
@@ -68,6 +68,8 @@ let proxiesByID
 let didWarnAboutID
 let hasCreatedElementsByType
 let idsByType
+let knownSignatures
+let didUpdateProxy
 
 const hooks = {
   register(type, uniqueLocalName, fileName) {
@@ -105,6 +107,7 @@ const hooks = {
       proxiesByID[id] = createProxy(type)
     } else {
       proxiesByID[id].update(type)
+      didUpdateProxy = true
     }
   },
 
@@ -113,23 +116,45 @@ const hooks = {
     didWarnAboutID = {}
     hasCreatedElementsByType = new ComponentMap(useWeakMap)
     idsByType = new ComponentMap(useWeakMap)
+    knownSignatures = {}
+    didUpdateProxy = false
   },
 }
 
 hooks.reset(typeof WeakMap === 'function')
 
+function warnAboutUnnacceptedClass(typeSignature) {
+  if (didUpdateProxy) {
+    console.error(
+      'React Hot Loader: this component is not accepted by Hot Loader. \n' +
+      'Please check is it extracted as a top level class, a function or a variable. \n' +
+      'Click below to reveal the source location: \n',
+      typeSignature
+    )
+  }
+}
+
 function resolveType(type) {
   // We only care about composite components
   if (typeof type !== 'function') {
     return type
   }
 
+  const wasKnownBefore = hasCreatedElementsByType.get(type)
   hasCreatedElementsByType.set(type, true)
 
   // When available, give proxy class to React instead of the real class.
   const id = idsByType.get(type)
   if (!id) {
-    return type
+    if (!wasKnownBefore) {
+      const signature = type.toString()
+      if (knownSignatures[signature]) {
+        warnAboutUnnacceptedClass(type)
+      } else {
+        knownSignatures[signature] = type
+      }
+    }
+    return type;
   }
 
   const proxy = proxiesByID[id]


Test patch:
diff --git a/test/patch.dev.test.js b/test/patch.dev.test.js
index a01c461..98dc375 100644
--- a/test/patch.dev.test.js
+++ b/test/patch.dev.test.js
@@ -52,6 +52,53 @@ function runAllTests(useWeakMap) {
       }
     })
 
+    it('report proxy failures', () => {
+      const f1 = () => <div>234</div>
+      const f2 = () => <div>123</div>
+      const f3 = () => <div>123</div>
+      const f4 = () => <div>123</div>
+
+      const dynamic = () => () => <div>123</div>
+
+      const spy = jest.spyOn(console, 'error')
+
+      try {
+        RHL.register(f1, 'f1', '/wow/test.js')
+
+        // emulate `static` components from node_modules
+        React.createElement(f1)
+        React.createElement(f1)
+        expect(console.error.mock.calls.length).toBe(0)
+        React.createElement(f2)
+        React.createElement(f2)
+        React.createElement(f3)
+        React.createElement(f3)
+
+        expect(console.error.mock.calls.length).toBe(0)
+
+        // emulate uncatched non-static component
+        React.createElement(dynamic())
+        expect(console.error.mock.calls.length).toBe(0)
+        React.createElement(dynamic())
+        expect(console.error.mock.calls.length).toBe(0)
+
+        // emulate HMR
+        RHL.register(f4, 'f1', '/wow/test.js')
+
+        const signature = dynamic()
+        React.createElement(signature)
+        expect(console.error.mock.calls.length).toBe(1)
+        expect(console.error.mock.calls[0][0]).toBe(
+          'React Hot Loader: this component is not accepted by Hot Loader. \n' +
+          'Please check is it extracted as a top level class, a function or a variable. \n' +
+          'Click below to reveal the source location: \n'
+        )
+        expect(console.error.mock.calls[0][1]).toBe(signature)
+      } finally {
+        spy.mockRestore()
+      }
+    })
+
     it('resolves registered types by their last ID', () => {
       RHL.register(A1, 'a', 'test.js')
       const A = <A1 />.type