Open rmorshea opened 1 year ago
On thinking about this a bit. I actually think the current design is flexible enough to accommodate signals:
from weakref import WeakSet
from idom.core.hooks import current_hook, COMPONENT_WILL_UNMOUNT_EFFECT
class Signal:
def __init__(self, value):
self._value = value
self._subscribers = WeakSet()
@property
def value(self):
try:
hook = current_hook()
except RuntimeError:
pass
else:
self._subscribers.add(hook)
hook.add_effect(COMPONENT_WILL_UNMOUNT_EFFECT, lambda: self._subscribers.remove(hook))
return self._value
@value.setter
def value(self, new):
for hook in self._subscribers:
hook.schedule_render()
self._value = new
The signal pattern seems preferable to contexts in most cases. Contexts have the benefit of being scoped to a given ContextProvider
which means that one context can have different values under different providers. To achieve the same thing with signals you'd just have to declare more of them. With that said, allowing the same context to have two different values seems like a confusing concept to wrap your head around, so maybe signals are better in this respect too.
Anyway, something to consider when it comes time to document contexts - perhaps we'd want to push the signal pattern instead.
The reasoning behind my assertion that signals are better comes down to two factors:
More reasons to use signals over standard hooks: https://emnudge.dev/blog/react-hostage#fixing-react
I'm realizing that signals could actually be better than the standard use_state
hook if integrated a bit more into ReactPy's core layout rendering logic. Instead of re-rending a whole component, if you pass a signal directly as a prop or child of an element, ReactPy could intelligently update the value for you without requiring the whole component to re-render.
For example, this would cause an infinite loop:
from time import sleep
from threading import Thread
@component
def InfiniteLoop():
count, set_count = use_state(0)
Thread(target=lambda: sleep(1) or set_state(state + 1)).run()
return html.p("Current value is:", count)
However, with signals...
from time import sleep
from threading import Thread
@component
def RendersOnce():
count = use_signal(0)
Thread(target=lambda: sleep(1) or signal.value := signal.value + 1).run()
return html.p("Current value is:", count)
The component RendersOnce
would, as the name suggests, only render once. Nonetheless the <p>
element would receive a single update after the expected delay.
@rmorshea You meant this right?
Thread(target=lambda: sleep(1) or count.value := count.value + 1).run()
count.value instead of signal.value.
Correct.
Current Situation
Preact, a fast and lightweight drop-in replacement for React, has a pluggable renderer that allows them to add new state and rendering primitives without a wholesale re-write. Signals are one of the new primitives they created. In this introductory article they lay out some advantages of signals over hooks.
Proposed Actions
While signals are an interesting pattern to explore, the more useful action here would be to implement signals for the purpose of developing a similarly pluggable layout rendering pipeline. This would ensure that, as better reactive patterns are developed, IDOM can more easily evolve with them.