react-refresh-webpack-plugin: Using extended classes for props doesn't get picked up by react-refresh

I have a curious case where I am using classes instead of plain old JS objects as input properties for my components.

Simple usage with classes like the following works as expected and maintains its state when I make changes to styling etc.

import { createElement, useState } from 'react' 
 
export class CounterProps {
    constructor(title) {
        this.title = title;
    }
}

export const Counter = (props) => {
  const [count, setCount] = useState(0)
  return createElement("div", null, 
    createElement("h1", null, props.title),
    createElement("h1", null, count), 
    createElement("button", { onClick: () => setCount(count + 1) }, "Increment")
  );
}

export const Counters = () => {
  return createElement("div", null, 
     createElement(Counter, new CounterProps("Page title")),
     createElement(Counter, new CounterProps("Another title"))
   );
}

However, once I extend CounterProps with anything then fast-refresh doesn’t pick up the changes anymore:

export class Anything { }

export class CounterProps extends Anything {
    constructor(title) {
        super();
        this.title = title;
    }
}

I am using the following versions (on Windows):

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 2
  • Comments: 22 (7 by maintainers)

Most upvoted comments

I don’t know why you’re doing this

@gaearon The reason is simple: this is how F# records are compiled to JavaScript using Fable and these F# records happen to be a really nice way to define lightweight React component prop types:

type NavigationItemProps = {
    title: string
    active : bool
}

// Component defition
[<ReactComponent>]
let NavigationItem (props: NavigationItemProps) = 
  let (hovered, setHovered) = React.useState(props.active)
  Html.h1 props.title

// call-site: 
NavigationItem { title = "Home"; active = false }

There are workarounds of course but I would really appreciate it if we could define React components in F# the following structure: F# source file is called {ComponentName}.fs and contains the props type definition along with the function itself. Potentially having multiple small components and their prop types in the same file when you have small components.

Hi @pmmmwh, first of all, thanks a lot for the detailed answer. I now understand why the refresh fails. However, from the perspective of the F# side of things (because I am really talking about JS which is transpiled from F#), it could be possible to add metadata to the class declaration which could very well easily allow fast-refresh to pick it up. Two questions arise:

  • Which metadata do you need? It could be a dummy, compiler-generated function with a special name like $IamClass which will be returned in the property names array when you ask Object.getOwnPropertyNames(Test.prototype) => ["constructor", "$IamClass"]

  • How to generically tell or hint the plugin that with that information? An option passed to the plugin would do the trick { classDetectionHints: ["$IamClass"] } or something similar.

What do you think?

Thanks for the reproduction - will investigate sometime this week (working on a new release at the moment, see if we can get this fixed in time …)

@gaearon I did some tests, and this is a bit tricky.

After transpilation, empty classes are de-sugared into this:

function _classCallCheck(instance, Constructor) {
  if (!(instance instanceof Constructor)) {
    throw new TypeError("Cannot call a class as a function");
  }
}

var Anything = function Anything() {
  _classCallCheck(this, Anything);
};

So, with respect to the current heuristic:

https://github.com/facebook/react/blob/e6a0f276307fcb2f1c5bc41d630c5e4c9e95a037/packages/react-refresh/src/ReactFreshRuntime.js#L674-L697

  • It will fail the first condition (type.prototype.isReactComponent does not exist)
  • It will fail the second condition (ownNames.length is 0 when de-sugared, 1 when kept intact, ownNames[0] is 'constructor' when not de-sugared)
  • It will fail the third condition (type.prototype.__proto__ === Object.prototype since it is an empty class/function)

The main question here is whether there exist a heuristic to differentiate empty classes from functions (especially considering when de-sugared to ES5), not the other way around (detecting a render function exists within a class).