preactjs / signals

Manage state with style in every framework
https://preactjs.com/blog/introducing-signals/
MIT License
3.82k stars 95 forks source link

Using `@preact/signals` with `hydrate` or `render` the state updates but the re-render never happens. #589

Closed na47io closed 3 months ago

na47io commented 3 months ago

Environment

describe the bug

When using @preact/signals the state updates but the re-render never happens.

Adding useState from preact/hooks, specifically invoking the setCounter callback forces the re-render (?) and fixes the issue.

to reproduce

See minimal example ```html broken app
```

steps

  1. open example code (above) in the browser
  2. mash the Increment button - observe nothing happening
  3. uncomment client.tsx:23, repeat button mash - observe rerender

expected behaviour

the component re-renders without having to useState.

rschristian commented 3 months ago

This is a configuration error, when using https://esm.sh, you must make sure you're marking Preact as external else you'll end up getting multiple copies -- Preact must be a singleton else hooks will not work, and signals rely upon hooks in order to function correctly. Here's an example with an import map, which is the recommended way to go about this:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width" />
    <script type="importmap">
      {
        "imports": {
          "preact": "https://esm.sh/preact@10.12.1",
          "preact/hooks": "https://esm.sh/preact@10.12.1/hooks?external=preact",
          "@preact/signals": "https://esm.sh/@preact/signals@1.1.3?external=preact",
          "htm/preact": "https://esm.sh/htm@3.1.1/preact"
        }
      }
    </script>
  </head>
  <body>
    <div id="root"></div>

    <script type="module">
      import { render } from 'preact';
      import { computed, signal } from '@preact/signals';
      import { useState } from 'preact/hooks';
      import { html } from 'htm/preact';

      // Create signals outside of the component
      const count = signal(0);
      const double = computed(() => count.value * 2);

      export function App() {
        const [hcount, setHcount] = useState(0);

        const add = (n) => {
          count.value += n;
          console.log(count.value); // the state is updating
          // UNCOMMENT LINE BELOW this to make signals work
          // setHcount((c) => c + n);
        };
        return html`
          <button onClick=${() => add(-1)}>Decrement</button>
          <input readonly size="4" value=${count.value} />
          <input readonly size="4" value=${double.value} />
          <button onClick=${() => add(1)}>Increment</button>
        `;
      }

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

Stackblitz example

na47io commented 3 months ago

Oh I see...Every day is a school day!

Thank you @rschristian! ๐Ÿ™

rschristian commented 3 months ago

No problem.

There's an open issue to add this information to our docs site (https://github.com/preactjs/preact-www/issues/1116); just out of curiosity, would that have helped? Did you go looking for that information on our website at all?

While I still need to get around to writing that page, I'm also trying to gauge where we might need to add links for it to catch as many users as possible.

na47io commented 3 months ago

I somewhat agree with https://github.com/preactjs/preact-www/issues/1116 description. When looking for a solution I saw a lot of people getting pwned by the singleton issue in different ways.

I came to preact first from fresh and now for preact/signals. I am new to the ecosystem and the way preact scales down to my use-case without sacrificing ergonomics sparked a lot of joy. To me at least that was not immediately obvious from skimming https://preactjs.com/

what would've helped

One idea could be a No Build Tools Walkthrough that incrementally adds complexity:

single html file -> add jsx -> add typescript -> SSR it -> Island it (?) -> Resumability it (??)

A doc like that could be useful for a newcomer like me looking for a quote-unquote principled approach. Since I am on this journey myself I might write this.

quick win?

I think changing the example code for https://preactjs.com/guide/v10/getting-started#no-build-tools-route slightly might get us most of the way.

<!DOCTYPE html>

<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Some App</title>
    <script type="importmap">
        {
          "imports": {
            "preact": "https://esm.sh/preact@10.12.1",
            "preact/hooks": "https://esm.sh/preact@10.12.1/hooks?external=preact",
            "@preact/signals": "https://esm.sh/@preact/signals@1.1.3?external=preact"
          }
        }
      </script>
</head>

<body>
</body>

<script type="module">
    import { h, render } from 'preact';
    import { signal } from "@preact/signals";

    const count = signal(0)

    // Create your app
    const app = () => {
        return h('div', null, [
            h(`h1`, null, `count: ${count.value}`),
            h('button', {
                onclick: () => {
                    count.value++;
                }
            }, "Increment")]
        );
    }

    render(h(app), document.body);
</script>

</html>

I speculate this example can align better with user's needs: adding reactivity instead of rendering static markup. This code then:

Then just move the NOTE from https://preactjs.com/guide/v10/getting-started#using-preact-with-htm-and-importmaps. And make it red ๐Ÿ˜… .

This is a few more LOC, but imho introduces essential concepts in a no-build setting. Getting Started will get a little busy and might need to link to sub-pages for the three cases.

That said, my issue would have resolved itself have I read the Getting Started section top-to-bottom as I should have ๐Ÿค“

If this sounds good I'm happy to pick up the issue.

rschristian commented 3 months ago

One idea could be a No Build Tools Walkthrough that incrementally adds complexity:

single html file -> add jsx -> add typescript -> SSR it -> Island it (?) -> Resumability it (??)

Well you're not getting JSX, TS, or (automatic) islands without build tooling -- fundamentally they require it. Not sure I really understand this.

I speculate this example can align better with user's needs: adding reactivity instead of rendering static markup. This code then:

  • introduces h
  • shows off @preact/signals
  • hits the importMap case that trips a lot of people.

I don't think this is really appropriate there, no. The point of that example is to show the bare-minimum you need to get up and running. We don't want to introduce something completely optional and unrelated like signals. The main point of import maps is to orchestrate dependencies of which that example has only 1, so it's largely unneeded IMO.

They definitely start being necessary when you add in hooks, signals, htm, etc., and those are the cases I want to document as they're a bit more advanced.

That said, my issue would have resolved itself have I read the Getting Started section top-to-bottom as I should have ๐Ÿค“

To be clear, I wasn't saying you should have read that! Even if you had, that section only touches on the idea, would be easy to miss and easy to incorrectly implement even if you guessed it was needed. Importmaps. while great, are not without headaches and are very fiddly and easy to mess up.

My question was more just seeing if docs there could have helped, as I want to make sure when they are added that they're as accessible as possible.


I do think a separate "Advanced" page is best, I want to avoid extending the "Getting Started" doc (as you mentioned) and keeping the examples there as minimal as possible -- that page really is meant to be "Here's how you can start writing Preact and set up aliases", nothing more. Components, hooks, signals, etc., can be left to other pages.

Really appreciate your feedback though, I'll try to get this on my todo list soon-ish.

rschristian commented 3 months ago

Going to close this out, further discussion can happen at https://github.com/preactjs/preact-www/issues/1116 as this is a docs issue more than a signals issue.

na47io commented 3 months ago

Well you're not getting JSX, TS, or (automatic) islands without build tooling

every day really is a school day ๐Ÿ˜…

I look forward to the new docs! I can tell i need to read em...

rschristian commented 3 months ago

TLDR: Lack of build tooling means you're left with standard JS and only standard JS -- JSX and TS are non-standard syntaxes, requiring transpilation before running, so they're out, and automatic islands require build tools to identify, wrap, and split out chunks as well as mount them on the client. Manual islands are still possible, but fiddly/easy to break and the DX ain't great.

rschristian commented 3 months ago

Wrote this up, would love to know if you have any thoughts (if you have the time): https://deploy-preview-1175--preactjs.netlify.app/guide/v10/no-build-workflows

na47io commented 3 months ago

@rschristian that's brilliant! Great idea with the common patterns section!

rschristian commented 3 months ago

Glad to hear it! It's linked from the 'Getting Started' docs, hopefully this will help in the future.