reactive-python / reactpy

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

`use_effect`'s unmount method is not always called with dynamically rendered children #1198

Open Archmonger opened 7 months ago

Archmonger commented 7 months ago

Current Situation

This issue was first discovered while trying to develop https://github.com/reactive-python/reactpy-django/pull/226

I created a component that simulates a list of conditionally rendered children. Each child in the list contains a unique key. However, the use_effect unmount method does not always appear to be called when the component is dismounted.

Here is a minimum viable example:

from uuid import uuid4

from reactpy import component, hooks, html, run

@component
def child():
    ownership_uuid = hooks.use_memo(lambda: uuid4().hex)

    @hooks.use_effect(dependencies=[])
    async def unmount_manager():
        print("registering new unmount func")
        def unmount():
            # FIXME: This is not working as expected. Dismount is not always called.
            print("unmount func called")
        return unmount

    return ownership_uuid

@component
def root():
    components = hooks.use_ref([child(key=uuid4().hex)])
    reload_trigger, set_reload_trigger = hooks.use_state(True)

    async def add_front_child(event):
        components.current.insert(0, child(key=uuid4().hex))
        set_reload_trigger(not reload_trigger)

    async def add_end_child(event):
        components.current.append(child(key=uuid4().hex))
        set_reload_trigger(not reload_trigger)

    async def remove_front_child(event):
        if components.current:
            components.current.pop(0)
            set_reload_trigger(not reload_trigger)

    async def remove_end_child(event):
        if components.current:
            components.current.pop()
            set_reload_trigger(not reload_trigger)

    return html.div(
        html.div(
            html.button({"on_click": add_front_child}, "Add End"),
            html.button({"on_click": add_end_child}, "Add Front"),
            html.button({"on_click": remove_front_child}, "Remove End"),
            html.button({"on_click": remove_end_child}, "Remove Front"),
        ),
        html.div(f"Components: {components.current}"),
        components.current,
    )

run(root)

You can demo this by adding a bunch of children, then removing them. If you look at terminal, you'll notice that unmount was only called once despite multiple children being unloaded.

Proposed Actions

No response