preactjs / preact

⚛️ Fast 3kB React alternative with the same modern API. Components & Virtual DOM.
https://preactjs.com
MIT License
36.57k stars 1.95k forks source link

Support for reactive properties and text nodes #3300

Closed fabiospampinato closed 1 year ago

fabiospampinato commented 2 years ago

Describe the feature you'd love to see

I'd like to have some ways to pass reactive values to Preact, for example instead of passing it a string as a child I'd like to pass it something like this:

function () {
  const node = document.createTextNode ( observable () );
  onChange ( observable, value => text.nodeValue = value );
  return node;
}

Basically so that when I know already that the value of that text node should change I can change it directly, bypassing Preact entirely.

The same would be useful for things like setting a property.

Maybe this use case could be enabled by adding some new options hooks?

Additional context (optional)

I could potentially already do this by using refs or returning DOM nodes directly from my component, but I'd rather do it much more cleanly than that, patching Preact once, without forking, and then just passing little observables around instead of primitives.

Maybe I could already get reactive text nodes by overriding document.createTextNode, but I'd rather not do that.

fabiospampinato commented 2 years ago

Something like this seems to work pretty well for text nodes actually:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Demo</title>
  </head>
  <body>
    <script type="module">
      import { h, html, useEffect, useMemo, useState, useCallback, render } from 'https://unpkg.com/htm/preact/standalone.mjs';
      import {observable, computed} from 'https://cdn.skypack.dev/sinuous/observable';

      const count = observable ( 0 );
      const increment = () => count ( count () + 1 );
      setInterval ( increment, 10 );

      function DomNode ({ children }) {
        this.shouldComponentUpdate = () => false;
        return Object.defineProperty ( h ( children.localName ), '__e', { get: () => children, set: Object } );
      }

      function Text ({ children }) {
        const node = useMemo ( () => document.createTextNode ( count () ), [] );
        useEffect ( () => computed ( () => node.nodeValue = count () ), [] );
        return html`<${DomNode}>${node}</>`;
      }

      function App () {
        return html`<p><${Text}>${count}</></p>`;
      }

      render ( html`<${App} />`, document.body );
    </script>
  </body>
</html>

Although maybe updating text nodes willy-nilly like that could cause performance issues 🤔 Is it possible to schedule this in sync with eventual mutations to the DOM from Preact's side?

I'd really like to do the same with classes and other attributes too, possibly without using refs or stopping using jsx/template tag.

fabiospampinato commented 2 years ago

Something like this sort of works for attributes too, but it uses refs:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Demo</title>
  </head>
  <body>
    <style>
      .red {
        color: red;
      }
    </style>
    <script type="module">
      import { h, html, useEffect, useRef, useMemo, useState, useCallback, render } from 'https://unpkg.com/htm/preact/standalone.mjs';
      import {observable, computed} from 'https://cdn.skypack.dev/sinuous/observable';

      const count = observable ( 0 );
      const color = observable ( true );
      const increment = () => count ( count () + 1 );
      const flash = () => color ( !color () );
      setInterval ( increment, 10 );
      setInterval ( flash, 10 );

      function useObservableAttribute ( ref, attr, observable ) {
        useEffect ( () => {
          if ( !ref.current ) return;
          return computed  ( () => ref.current.setAttribute ( attr, observable () ) );
        }, [ref.current] );
      }

      function DomNode ({ children }) {
        this.shouldComponentUpdate = () => false;
        return Object.defineProperty ( h ( children.localName ), '__e', { get: () => children, set: Object } );
      }

      function Text ({ children }) {
        const node = useMemo ( () => document.createTextNode ( count () ), [] );
        useEffect ( () => computed ( () => node.nodeValue = count () ), [] );
        return html`<${DomNode}>${node}</>`;
      }

      function App () {
        const ref = useRef ();
        useObservableAttribute ( ref, 'class', () => color () ? 'red' : '' );
        return html`<p ref=${ref}><${Text}>${count}</></p>`;
      }

      render ( html`<${App} />`, document.body );
    </script>
  </body>
</html>

I'm open to ideas on how to do this better 🤔

marvinhagemeister commented 1 year ago

Closing as this is what @preact/signals is doing.