preactjs / signals

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

RFC: async in signals #291

Closed JoviDeCroock closed 1 year ago

JoviDeCroock commented 1 year ago

When using the core signals library we are enabled to perform actions like

let isLoading = signal(true);
// Hypothetical deep-signal to react to objects
let result = deepSignal(undefined)

fetch(x).then(...)

however this is a lot of boilerplate code for something that could probably be both optimized and made ergonomic from our package standpoint. The issue here arises when we want to implement this flow in the core Preact library, we would have to resort to hooks/... so we're able to implement a similar flow.

Personally I have been a fan of the createResource API in Solid. Going from this thinking I would suggest us following a similar routes as the resource way of thinking, in practice this could look something like

const { isLoading, error, result } = createResource(asyncOperation);

This however leads me to a few thinking points as we have the open questions around whether or not we leverage Suspense as a loading mechanism and how do we make this work on the server? Currently we have preact-ssr-prepass that catches an async load entry and resolves as needed, this would not work if we don't op for the Suspense approach.

If we're opting out of Suspense we would probably need to make ourselves aware of loads when we are in a Node Environment so they can be awaited before rendering to a string (most likely would also need a way to be collected so it can be rehydrated if needed).

One last need here would be a reliable way to diff POJO signals as most results from async operations would lead to an object-like result.

This might be a stretch to support in Signals but as it currently stands I feel like there is no way outside of using hooks to reliably execute async operations.

eddyw commented 1 year ago

What about something like this?

const signal = asyncSignal(defaultValue); // signal.status = "RESOLVED"
const signal = asyncSignal(Error(""));    // signal.status = "REJECTED"
const signal = asyncSignal();             // signal.status = "PENDING"

await fetch(foo)
  .then(signal.then)
  .catch(signal.catch)

signal.status; // RESOLVED | REJECTED | PENDING
signal.value;  // null | T
signal.error;  // null | Error

// Programmatically change values:
signal.value = "foo"; // signal.status => "RESOLVED"
signal.error = "";    // signal.status => "REJECTED"

Then it's easier to create other abstractions on top of this.