WebReflection / usignal

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

untrack() #18

Closed kyr0 closed 1 year ago

kyr0 commented 1 year ago

Hi,

is there a recommended way to untrack()?

I have an effect that writes to a signal that I want to untrack after the first write so that it doesn't go in the way of the other signal writes that would otherwise re-trigger the effect. (the write happens in an effect which is necessary in that context, but due to the fact that it in an effect call, it is tracked and retriggers. It doesn't auto-retrigger because the signal is memoized but when other pieces of the code write to the value of the signal, this effect is triggered and overwrites again)

In Solid you have untrack() but here I just found one such function in the tests.

Thanks btw for the project, I started the same project 2 weeks ago because I had the same pain... and then found usignal :)

Best

kyr0 commented 1 year ago

Solved it with a peek() and conditional for the moment, but a way to untrack() the effect after the first write to slugToCreate.value would be nicer -- something like effect() is passed a function reference and if you call it it untracks or so:


const simplifiedExample = () => {

  const slugToCreate = useSignal('')

  const onInput = (event) => (slugToCreate.value = event.target.value)

  effect(async () => {
    if (slugToCreate.peek() === '') {
      slugToCreate.value = await getSlugToCreate()
    }
  })

return (<input
    type="text"
    value={slugToCreate.value}
    onInput={onInput}
    placeholder="Type here.."
    class="input input-bordered w-full max-w-xs"
  />)
}

Imagine:


const simplifiedExample = () => {

  const slugToCreate = useSignal('')

  const onInput = (event) => (slugToCreate.value = event.target.value)

  effect(async (untrack) => {
      slugToCreate.value = await getSlugToCreate()
      untrack()
  })

return (<input
    type="text"
    value={slugToCreate.value}
    onInput={onInput}
    placeholder="Type here.."
    class="input input-bordered w-full max-w-xs"
  />)
}
WebReflection commented 1 year ago

beside the fact that peek() is the solution to these and other cases where side-effects are not always desired (there is your untrack ... there's nothing to untrack if you don't track it in the first place), if the value is the same nothing happens to the signal or its chain of effects so I am having really hard time to understand what is it that you are trying to solve ... does the subscription bother you, even if the value is always the same? is the value not always the same but you want it to side-effect only the first time? In latter case, you have the solution, and there's nothing for me to do, so I can close it, I suppose, but feel free to add more context in case it needs to be re-opened.

P.S. peek() is not a hack, it's the way to go here, in Preact signals, or @webreflection/signal when no side-effects are meant, but the value is needed.

kyr0 commented 1 year ago

It was the latter case (only change it once, reactively), but it's not so beautiful, because the solution to not get it tracked in the first place is not always applicable -- it was in this case but the intention in the code is not clear. It says "if not empty" instead of "only want to have it reactive 1 time". And there are so many other cases where you are running code in an effect and at some point in time you'd like to stop that tracking from happening. Imagine you didn't think of that before -- so you can now try and refactor your code with many conditionals to solve it -- but arguably, it would be a much better DX if you would just be able to call an untrack() at any time, and be fine with an easy escape. Maybe you want 3 calls in 20 seconds and then untrack. How do you solve such cases? With a lot more signals where you aggregate that state and then make it conditional. But thats a lot of code and complexity compared to a potential untrack() call. I don't know if thats easy to implement though.

WebReflection commented 1 year ago

Also relevant: adding an untrack() to a signal is footgun prone because signals can be passed along many components or functions and untracking it somewhere might result into undesired behavior elsewhere ... if the untrack should be targeted only for the current effect you'll have bad/ugly performance due extra list of tracking effects attached to a signal ... using a conditional peek() is fine both for read and write operations which is why I believe you should not think what Solid does, as that's another world/pattern/solution based on tooling and AOT speculations, here I am following closer Preact signals but also I think if there's a solution, there's no need to bloat the library.

WebReflection commented 1 year ago

there are so many other cases where you are running code in an effect and at some point in time you'd like to stop that tracking from happening

name one ... 'cause I don't understand this use case to start with ... you either are interested in a signal, or you should just pass a state instead and handle that (or use events, which should not be forgotten as pattern for reactivity).

kyr0 commented 1 year ago

Also relevant: adding an untrack() to a signal is footgun prone because signals can be passed along many components or function and untracking it somewhere might result into undesired behavior elsewhere ... if the untrack should be targeted only for the current effect you'll have bad/ugly performance due extra list of tracking effects attached to a signal ... using a conditional peek() is fine both for read and write operations which is why I believe you should not think what Solid does, as that's another world/pattern/solution based on tooling and AOT speculations, here I am following closer Preact signals but also I think if there's a solution, there's no need to bloat the library.

Okay, I agree with you. The arguments here are probably to be valued more than a slight DX improvement for edgy cases. And tbh the Solid speculations and the compiler and all gave me many footguns when I reimplemented Next.js with Solid as its core. It's not public yet, but I'll soon publish it. I was recently thinking to just throw out Solid again, because I would gain the same results by just combining my JSX runtime (springtype) with a reactivity library, but it would be much less complex (as I said, I was writing it but then I paused that because of reasons and now I found this lib of yours :)

WebReflection commented 1 year ago

more on my thoughts:

// solve one-off empty to not-empty
if (!(state.key = signal.peek()))
  state.key = signal.value;

// solve X amount of changes
if ((state.changes = signal.peek()) < 3)
  state.changes = signal.value;
kyr0 commented 1 year ago

Yep, that would do in most cases. Thank you for your time!

kyr0 commented 1 year ago

Maybe one last question: What are your thoughts on signal based state management using Proxies? I was thinking to extract and implement something like https://www.solidjs.com/docs/latest/api#createmutable -- I like the DX of it alot; it's intuitive and not too bad in performance for most real world use-cases; the point here is the depth as the "reactivity Proxy" would be active on each level of the whole object tree

WebReflection commented 1 year ago

does that trigger updates on effects interested in just store.name when store.surname = 'changed' happens?

edit just trying to understand if that works with a single signal behind the scene or a signal per property

P.S. Proxy is not slow ... it's slow compared to non proxied objects but still not that slow in general, quite a good primitive for a scripting language as JS is.

WebReflection commented 1 year ago

the case all effects are triggered when any change happens to the store:

const handler = {
  get: (target, name) => target.value[name],
  set(target, name, value) {
    if (target.peek()[name] !== value)
      target.value = {...target.peek(), [name]: value};
    return true;
  }
};

const creteMutable = target => new Proxy(signal(target), handler);
WebReflection commented 1 year ago

the case each store value, including accessors, are handled differently, could be handled as such (haven't tried it yet, but I think it's a way to make it work):

function createSignal([name, descriptor]) {
  const shrinked = {
    accessor: !descriptor.hasOwnProperty('value'),
    signal: signal({$: this})
  };
  if (shrinked.accessor) {
    shrinked.get = descriptor.get;
    shrinked.set = descriptor.set;
  }
  return [name, shrinked];
}

const handler = {
  get(map, name) {
    const descriptor = map.get(name);
    const {value: {$}} = descriptor.signal;
    return descriptor.accessor ?
      descriptor.get.call($) :
      $[name];
  },
  set(map, name, value) {
    const descriptor = map.get(name);
    const {accessor, signal} = descriptor;
    const $ = signal.peek();
    let changed = !accessor && (value !== $[name]);
    if (accessor) {
      batch(() => {
        const p = new Proxy($, {
          set($, name, value) {
            if ($[name] !== value) {
              changed = true;
              $[name] = value;
              map.get(name).signal.value = {$};
            }
          },
          valueOf: () => $
        });
        descriptor.set.call(p, value);
      });
    }
    else
      $[name] = value;
    if (changed)
      this.signal.value = {$};
  }
};

const creteMutable = target => new Proxy(
  new Map(
    Array.prototype.map.call(
      Object.entries(
        Object.getOwnPropertyDescriptors(target)
      ),
      createSignal,
      target
    )
  ),
  handler
);
WebReflection commented 1 year ago

Btw, I thought untrack was kinda permanent but it looks lik eit's exactly a peek() so my comment on undesired side effects might be misleading. Apologies for that.

kyr0 commented 1 year ago

Hi :) sry for the long delay, there was been quite a bunch of stuff going on irl...

Thank you, yeah, I realized that too and started playing with different reactive store ideas in my head and as PoCs

I ended up separating the concern of "observation of change" on an object and the concern of "reactivity" and ended up building this:

https://github.com/jsheaven/observed

and on top that:

https://github.com/jsheaven/reactive

It's not perfect and couldn't be compared to the power and quality of usignal but yeah, it's a very simple "nano" approach with a different idea in mind -- opt-in reactivity, but still, "true" reactivity :)