matthewp / haunted

React's Hooks API implemented for web components 👻
BSD 2-Clause "Simplified" License
2.6k stars 92 forks source link

Hot module replacement #217

Open LarsDenBakker opened 3 years ago

LarsDenBakker commented 3 years ago

Hi there! At open-wc we started working on hot module replacement with es module workflows: https://open-wc.org/docs/development/hot-module-replacement/

So far it works for class based components. We're now looking into how we can support this for function based components.

The idea is that the web component base class implements a static and/or instance hotReplaceCallback function which does the actual replacement. Since you don't want this code to end up in production, projects can make it available through a module which patches the base class.

Since haunted in the end creates a class, I was hoping to do the same here. However one issue I ran into is that the class is created in a closure, so I can't inject any code into it. Also, the renderer function is only available within the closure.

I made some modifications to the source code and the basic features are working, so that looks promising. I did run into some issues with preserving state between replacements, so that would be something to look into.

Is this something that would be interesting to support in haunted?

matthewp commented 3 years ago

Hey Lars, thanks for starting the discussion. Is there a reason why you can't do something like this?

import { component } from 'haunted';
import hot from 'hot-web-components';

function App() {
 ...
}

const AppElement = hot(component(App));

ie, wrap the class that component() creates?

LarsDenBakker commented 3 years ago

@matthewp I did some rework, and indeed that's how it works now.

I got it working now for Haunted using a small patch: https://github.com/open-wc/open-wc/blob/master/packages/dev-server-hmr/src/presets/haunted.js#L3

Ideally I would like to patch the hotReplacedCallback onto some base class, would it possible to expose this from within Haunted? I could patch it onto each haunted component individually, but a user might be using multiple component base classes so we'd have to detect that something is a haunted component which makes things more complicated.

A bigger problem is updating the scheduler with a new renderer. Would it be ok to expose the renderer as a static field on the class? Then I could grab it there. I was able to work around it by instantiating a temporary element and getting it from the instance.

matthewp commented 3 years ago

Ideally I would like to patch the hotReplacedCallback onto some base class, would it possible to expose this from within Haunted?

Not sure I follow, what change are you suggesting?

A bigger problem is updating the scheduler with a new renderer. Would it be ok to expose the renderer as a static field on the class?

Yeah that's fine, but the scheduler is a privatish field at the moment. We should probably make it public if library are going to depend on it.

pyrossh commented 3 years ago

@LarsDenBakker Don't know if you are still looking into it. This is how I got esm hmr working in my system.

const tag = 'my-counter';
const Module = await import("/assets/my-counter.js");
document.querySelectorAll(tag).forEach((node) => {
  const Scheduler = class extends BaseScheduler {
      constructor(renderer, frag, host) {
          super(renderer, host || frag);
          this.frag = frag;
      }
      commit(result) {
          render(result, this.frag);
      }
  }
  node._scheduler = new Scheduler(Module.default, node);
  node.connectedCallback();
}