solidjs / solid-refresh

MIT License
86 stars 18 forks source link

Idea: Full Granular Mode #35

Closed lxsmnsyc closed 9 months ago

lxsmnsyc commented 1 year ago

This is just a draft discussion for the near minor release after #34

The focus here would be to make it so that /* @refresh granular */ is now the default refresh behavior, but even more so the next refresh architecture would change everything.

The Default (Non-Granular) Refresh

The default mode currently for solid-refresh. The behavior is that if the file changed, the components will just remount in-place. That's it. The issue here is that HMR can happen even if the file is unchanged, so if the user even saved the file without doing anything, components in the file would just remount.

The Old Granular Refresh

The old (current) granular refresh takes refreshing to the next level, in such a way that components refresh as if the components are located in separate files. The current granular refresh would keep track of the component's code by producing a hash (using xxHash32) as a "signature" for the component. This signature is then compared between HMR updates and then the registry decides if will remount the component if it changed, code-wise.

Code checks is good, but not accurate. It just so happens that external factors may change but the code doesn't, so the refresh must know it as well, which is why the current granular refresh also scans for external bindings: variables that aren't locally declared. The collected bindings is then emitted as an object. HMR then compares the object by key-value (think of dependency lists but out-of-order comparison) at which the HMR decides if the component should remount.

With both code check and dependency check, it as if granular refresh is emulating the components as some sort of independent modules, to which I call it Hot Component Replacement too.

Current granular refresh looks like this:

const Foo = $$component(REGISTRY, 'Foo', function Foo() {
  return <h1>Hello {message}</h1>;
}, {
  signature: <hash>,
  dependencies: { message },
});

The New Granular Refresh

The old Granular Refresh is quite nice, but it could be better. Old granular refresh is capable of cross-HMR state preservation (in a way) because it doesn't remount the component aggressively, but what if refresh doesn't have to remount the component at all?

The new Granular Refresh takes it to another step by not refreshing the component, but refreshing it's content. The compiler will try to separate the component's implementation into two parts: setup and template, in which then the same analysis (code signature and dependency tracking) is done to the each part.

For example, here's input code and the theoretically compiled code

function Counter() {
  const [count, setCount] = createSignal(0);

  function increment() {
    setCount((c) => c + 1);
  }

  function decrement() {
    setCount((c) => c - 1);
  }

  return (
    <div>
      <h1>Count: {count()}</h1>
      <button onClick={increment}>Increment</button>
      <button onClick={decrement}>Decrement</button>
    </div>
  );
}

which produces

const _setup1 = $$setup(REGISTRY, 'Counter', (props) => {
  const [count, setCount] = createSignal(0);

  function increment() {
    setCount((c) => c + 1);
  }

  function decrement() {
    setCount((c) => c - 1);
  }

  return {
    count,
    increment,
    decrement,
  };
}, {
  signature: <hash>
});

const _template1 = $$template(REGISTRY, 'Counter', (props, scope) => (
  <div>
    <h1>Count: {scope.count()}</h1>
    <button onClick={scope.increment}>Increment</button>
    <button onClick={scope.decrement}>Decrement</button>
  </div>
), {
  signature: <hash>
});

var Counter = $$component(REGISTRY, 'Counter', _setup1, _template1);

In new Refresh, Counter doesn't have to re-mount, it's setup and template can re-evaluate independently (depending on change) but can still communicate as one by making it so that setup returns a "lexical scope" through a reactive proxy. setup can also update independently that template doesn't have to re-render, which means components down the tree won't have to remount when necessary.

Known challenges:

lxsmnsyc commented 9 months ago

It's one step close with 0.7.0, but I'll be closing this now since the theoretical API isn't going to be leading this.