flow icon indicating copy to clipboard operation
flow copied to clipboard

A new API that allows triggering the Signal.runEffect

Open abdullahtellioglu opened this issue 7 months ago • 9 comments

Describe your motivation

Updating a property of a signal does not trigger the Signal.effect as the value does not change. There should be a way to trigger Signal.runEffect something like requestUpdate in Lit.

Describe the solution you'd like

Signal.trigger() or a similar API that invokes all the runEffects that Signal uses.

Additional context

Not the same purpose, but another signal type can be proposed that compares value deeply, rather than checking the object itself

abdullahtellioglu avatar Jun 03 '25 07:06 abdullahtellioglu

How about using the update method of value signal: ValueSignal<Person>::update(person -> { person.setAge(); return person; })?

There is no way to do it in this way currently, you need to use update:

valueSignal.value().setAge(...);

mshabarov avatar Jun 03 '25 10:06 mshabarov

That might work, but semantically requestUpdate() looks better. You might not have anything to update as the project can be a mix of both Signal and non-Signal solutions. The urge to have such an API came when working with Map actually.

abdullahtellioglu avatar Jun 03 '25 10:06 abdullahtellioglu

Why do you want to run the effect again if none of its inputs have changed? It would then just do the same thing again which seems pointless.

Legioth avatar Jun 05 '25 07:06 Legioth

Or do you mean that the value of your signal is a mutable object and you're changing some property of that object? If that's the case, then your suggestion will not work because the value in the signal is always a deep clone of the original object.

Legioth avatar Jun 05 '25 07:06 Legioth

Or do you mean that the value of your signal is a mutable object and you're changing some property of that object? If that's the case, then your suggestion will not work because the value in the signal is always a deep clone of the original object.

AFAIR, changing a property of a complex object manually does not trigger the Signal.

Furthermore, I encountered the issue while working with MapSignal where changing a property of a value that is a complex type does not trigger the Signal to take effect. So, I had to remove MapSignal and create another POJO class that contains all possible entries in it, and use ValueSignal instead. There is an internal conversation in Slack.

abdullahtellioglu avatar Jun 05 '25 08:06 abdullahtellioglu

changing a property of a complex object manually does not trigger the Signal.

It goes further than that: the signal value still has the old property value. The value stored in the signal is a deep clone (or actually a JSON tree created from the value) which means that changes you make to the original object will not be in the signal value. It's a "value" signal, not a "reference" signal.

complexObject.setFoo("foo");
signal.value(complexObject);
// signal.value() == complexObject is false

complexObject.setFoo("bar");
// signal.value().getFoo() still returns "foo"

You always need to run signal.value(complexObject); again after making any change to the object if you want the value returned by signal.value() to contain those changes.

At this point, I would recommend to track each individual value in its own signal instance rather than collecting them all into a bigger object. We probably need to provide some additional helpers to make that easier but we need some practical experience from various cases before we can know what it should look like.

Legioth avatar Jun 05 '25 09:06 Legioth

And this is by the way similar to how you should use signals or useState in React. If you have e.g. an array of items, then you need to do [...oldArray, newItem] to create a new array rather than oldArray.push(newItem). The same also goes for objects with e.g. {...oldObject, foo: newFooValue} rather than just oldObject.foo = newFooValue. Unfortunately, Java doesn't provide the same syntactical sugar for creating a new instance based on destructuring an old one.

There is ListSignal and MapSignal that provides helpers around some generic operations (e.g. adding a new item to a list) but anything based on ValueSignal (as a standalone signal or as a child of a ListSignal or MapSignal) must treat the value as immutable and create a new instance to make any changes.

Legioth avatar Jun 05 '25 09:06 Legioth

React useState uses references to an object, but as you stated, Signal uses value instead, but it does not update the complex object signal under the hood. You need to call the signal.value explicitly.

If I have multiple objects, I need to call signal.value(thisObject), signal.value(thatObject), signal.value(....). However, if Signal provides an API that ensures the effects will be invoked with the latest data, that would be very convenient. Lit has @state annotation that invokes component update and also gives you the ability to update the component manually by calling requestUpdate()

abdullahtellioglu avatar Jun 05 '25 10:06 abdullahtellioglu

Even though React uses a reference, you're still strongly encouraged to treat is as an immutable value.

For now, I would recommend to use a dummy counter signal that you always read in your effects and increment whenever you want to run the effects again. I think we need a better understanding of use cases before it would make sense to introduce any dedicated API.

Legioth avatar Jun 05 '25 10:06 Legioth

This is really just one out of several features specifically for the ReferenceSignal idea that I described in https://github.com/vaadin/flow/issues/22648.

Legioth avatar Nov 03 '25 12:11 Legioth