lit / rfcs

RFCs for changes to Lit
BSD 3-Clause "New" or "Revised" License
15 stars 10 forks source link

[RRFC] Stateful/reactive render functions #32

Open sorvell opened 10 months ago

sorvell commented 10 months ago

Motivation

Functions are the simplest unit of composition and the first tool a developer leverages to accomplish almost any task. lit empowers developers to write shareable interoperable code that creates rich interactive interfaces. Minimizing the distance and friction between starting with a function and ending with a great UI serves that goal.

There are 2 main pieces of this puzzle: (1) describing rendering, (2) managing reactive state.

Describing rendering: Lit's html and css TTL syntaxes are excellent at this, and can easily be returned from a function.

Managing reactive state:

This last issue is addressed below...

Example

Consider creating a simple counter:

In a LitElement, it looks like:

@state()
accessor count = 0;

render() {
  return html`<button @click=${() => this.count++}>${this.count}</button>`;
}

With a function using signals, this can be:

const renderCounter = () => html`<button @click=${() => count.value++}>${count}</button>`;

But this doesn't work because we need to get the count signal from somewhere.

How

React solves this problem with hooks, but these have tradeoffs. Lit can make this more straightforward by leveraging the fact that all Lit templates are rendered into persistent "parts," and access to them is provided via the directives API.

So, all we need is a simple directive that can initialize state. Here's the updated counter example:

const renderCounter = (use) => {
  const count = use.state(() => signal(0));
  return html`<button @click=${() => count.value++}>${count}</button>`;
}

Then use it like this:

  return html`counter: ${stateful(renderCounter)}`

See working prototype.

The stateful directive provides a state method which memoizes the result of its argument.

References

sorvell commented 10 months ago

Using this stateful directive can unlock powerful behavior like this prototype, TLDR below:

const renderComments = (use, dataId) => {

  const stages = use.state(() => {

    const button = createRef();
    const buttonClicked = interactive(button); // separate helper

    return [
      // initial
      html`<button ref=${button}>Load Comments...</button>`,
      // loading
      new Promise(async (resolve) => {
        await buttonClicked;
        resolve(html`Loading...`);
      }),
      // comments
      new Promise(async (resolve) => {
         await buttonClicked;
         await import('comments'); // import-mapped
         const comments = await fetch(`commentsUrl/?id=${dataId}`);
         resolve(html`<x-comments .comments=${comments}></x-comments>`);
     })
     ].reverse();   
  });

  return html`${until(...stages)}`;
}

// ...
html`${stateful(renderComments, 5)}`;
maxpatiiuk commented 2 months ago

Would be great if Lit were to use @lit-labs/preact-signals (or native signals once widely available) to make stateful function components a thing