reactive-python / reactpy

It's React, but in Python
https://reactpy.dev
MIT License
7.82k stars 317 forks source link

Look into Signals Pattern #854

Open rmorshea opened 1 year ago

rmorshea commented 1 year ago

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.

rmorshea commented 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
rmorshea commented 1 year ago

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.

rmorshea commented 1 year ago

The reasoning behind my assertion that signals are better comes down to two factors:

  1. No need for a context provider - one less thing you need to learn
  2. Only components which used the signal get rendered on a change - more performant
rmorshea commented 1 year ago

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.

VatsalJagani commented 1 year ago

@rmorshea You meant this right?

Thread(target=lambda: sleep(1) or count.value := count.value + 1).run()

count.value instead of signal.value.

rmorshea commented 1 year ago

Correct.