Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RRFC] Stateful/reactive render functions #32

Open
1 task done
sorvell opened this issue Nov 18, 2023 · 1 comment
Open
1 task done

[RRFC] Stateful/reactive render functions #32

sorvell opened this issue Nov 18, 2023 · 1 comment

Comments

@sorvell
Copy link
Member

sorvell commented Nov 18, 2023

  • I searched for an existing RRFC which might be relevant to my RRFC

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:

  • Lit's reactive properties provide an excellent mechanism for managing reactive state and make sense to use when creating custom elements that extend LitElement. If a dev started with a function, they must move to a class, and this is fine when it makes sense.
  • However, the new @lit-labs/preact-signals library provides an alternative for simpler use cases and it more seamlessly builds on top of functions.
    • Signals are reactive state containers. Their reactivity does require using them within a scope that records dependencies (aka effects), but the lit signals package provides SignalWatcher and watch to deal with this.
    • The one remaining problem is having a convenient way to create signals within a function. They can certainly be passed in as arguments, but what if they are internal the ui being managed by the function?

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

  • n/a
@sorvell
Copy link
Member Author

sorvell commented Nov 18, 2023

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)}`;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant