WebReflection / usignal

A blend of @preact/signals-core and solid-js basic reactivity API
ISC License
212 stars 14 forks source link

Expose .subsribe method? #15

Closed dy closed 1 year ago

dy commented 1 year ago

@preact/signals expose .subscribe method as part of external interface (although undocumented), which makes it effectively a Subscribable - pattern shared by Rxjs, Observable and many variations.

Would that be within usignal's intention to get this uniformity feature by exposing that method?

WebReflection commented 1 year ago

I’m not sure I understand the need or what it solves … any example that could help me providing an answer?

dy commented 1 year ago

One use-case is sube - it subscribes to any kind of reactive/observable, so that the reactive value can be used in eg. various template engines as field values. Eg. hyperf supports reactive fields in HTML fragments, or templize supports reactive fields in template-parts.

Sorry I may not have examples outside of projects within my scope, but speculatively anywhere where generic-observable values are supported usignal could be one of backing libs. I guess it's trivial to detect or wrap, just was curious if there's an intention to be a bit more uniform.

WebReflection commented 1 year ago

Can you give me an example of how it works in practice? Only “subscribers” right now are outer effects, so to me it looks like you can subscribe to a value by using the fx (also undocumented) utility to be notified when the value changed … these are lightweight and independent from effects chains (detached) so maybe that’s the same thing just with different name or implementation? I really need to see code to understand what is this about, I’m not crawling all those libraries here, mostly because I’ve no time

dy commented 1 year ago

Seems same name, yes.

The example I'm having atm is:

// scope is dict of signals
let evaluate = parseExpr(expr)
const result = computed(() => evaluate(scope))
result.subscribe(update)

That's same as

// scope is dict of signals
let evaluate = parseExpr(expr)
effect(() => update(evaluate(scope)))

But the latter is a bit more difficult to grasp and it's less flexible in terms of chosing backing reactive library - computed seems to be broader pattern than effect, is it?

WebReflection commented 1 year ago

Computed here are lazy but I don’t know what’s update in your first example

dy commented 1 year ago

update is directive-specific update function

Mex505 commented 1 year ago

Edited due lack of respect

WebReflection commented 1 year ago

@Mex505 i don’t tolerate these kind of exchanges and I will block you if this happens again.

dy commented 1 year ago

@WebReflection sorry couldn't elaborate yesterday from the phone. 'update' is a side-effect function, it is provided externally by user or library.

dy commented 1 year ago

@WebReflection ok, just faced another scenario. Imagine persistency for simple todo-list.

We need to save todos to local storage any time list or any particular item updates.

let todos = signal([])

// direct effect here serializes any time todos change
effect(() => save(todos.value))

function addItem(text, done) {
  let item = { text: signal(text), done: signal(done) }
  todos.value = [...items.value, item]

  // indirect effects here save full list any time any todo item changes.
  item.text.subscribe(() => save(todos.value))
  item.done.subscribe(() => save(todos.value))
}

function save(items) {
  localStorage.setItem('todomvc.items', JSON.stringify(items))
}

How would you write indirect effects here? I imagine you'd need to artificially insert dependency like

  effect(() => (item.text, item.done, save(todos.peek())))

But then we have to use todos.peek() here to avoid extra updates.

WebReflection commented 1 year ago

You can run effects and detach effects via the exported Fx class … it looks like everything is there but I need to document and provide examples … I also need to check what preact does with signals but to me it looks like you want to subscribe to a signal and run something on its value changed, which is a different, straight to the point, api, imho.

subscribe(callback, …signals)

would likely be my pick

dy commented 1 year ago

tbh I expected 'effect' to do that, just explicitly accepting dependencies as after-arguments. 'effect(callback, ...deps)'. That also looks somewhat like useEffect hook.

WebReflection commented 1 year ago

Except effects run if you access value which doesn’t happen by default or otherwise. Computed are lazy, so I really need time to understand what Preact does and it won’t happen today.

WebReflection commented 1 year ago

@dy OK, I've had a look at Preact subscribe and I think this is all you need?

import {Fx, Signal} from 'usignal';

const options = {async: false, equals: true};
Signal.prototype.subscribe = function (fn) {
  const fx = new Fx(() => { fn(this.value); }, void 0, options).run();
  return () => { fx.stop() };
};

I am not sure this should be exported though, as I don't want to mimic strictly everything Preact does (like a fork) but especially not undocumented features.

WebReflection commented 1 year ago

P.S. please keep in mind computed here are lazy, meaning you won't likely get any notification unless you access these ... if it's Preact internals you are after though, I suggest you just use Preact for your project as lazy computed are here to stay for the time being (these were not lazy before, horrible performance and side-effects all over).

WebReflection commented 1 year ago

@dy closing as this seems to be solved.