Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Interactive widgets have state binded to render order as opposed to Element object #651

Open
rileythai opened this issue May 16, 2024 · 4 comments
Labels
documentation Improvements or additions to documentation

Comments

@rileythai
Copy link

rileythai commented May 16, 2024

If widgets are displayed iteratively by pulling them from a list of Element objects, their state is non-unique. If a widget preceding in the list is deleted, it overrides the state(s) of the ones ahead of it.

An example is attached, along with a demo video. The state variable text appears to be binded not to the Element instance but the render order.
Screencast from 16-05-24 10:49:59.webm

import solara as sl
from solara.hooks.misc import use_force_update


@sl.component()
def WidgetInteractive():
    # state variable
    text, set_text = sl.use_state("")

    # interactive elements to set 'text' state variable
    sl.InputText(label="enter text", value=text, on_value=set_text)
    sl.Markdown(text)


widget_list = sl.reactive([WidgetInteractive()])


@sl.component()
def Page():
    # used to force rerender on add because it doesn't update automatically
    updater = use_force_update()

    def add():
        widget_list.value.append(WidgetInteractive())  # add element to list
        updater()  # call update
        return

    def remove_first():
        widget_list.value = widget_list.value[
            1:]  # specifically removes widget_list[0]

    with sl.Row():
        sl.Button(label="Remove first", on_click=remove_first)
        sl.Button(label="Append another", on_click=add)

    # shows length of widget_list
    sl.Markdown(f"Current length = {len(widget_list.value)}")

    # it doesn't display if you just drop the i there
    for i in widget_list.value:
        sl.display(i)


Page()

Is this because this implementation method is incorrect (and this is the expected behaviour), or is this a limitation because of how the underlying memory address works?

Version = 1.32.2 -- have not checked for bleeding edge because the solara-server cannot be found for the repo.

@maartenbreddels
Copy link
Contributor

Hi,

good question. This is currently not yet documented in solara, but by default the 'entity' is determined by the position on the child index (in this case this happens via display).

This means that you cannot 'prepend' or remove from the first position using this pattern. However, by calling .key(..) we can give an element a unique key, which then determines which state/context belongs to which position.
This is similar in react and vue, and something we really need to document.

I hope this example clarifies it a bit:

import solara as sl
from solara.hooks.misc import use_force_update


@sl.component()
def WidgetInteractive():
    # state variable
    text, set_text = sl.use_state("")

    # interactive elements to set 'text' state variable
    sl.InputText(label="enter text", value=text, on_value=set_text)
    sl.Markdown(text)


widget_list = sl.reactive([WidgetInteractive()])
counter_unique = sl.reactive(0)

@sl.component()
def Page():
    def add():
        key = f'widget-{counter_unique}'
        counter_unique.value += 1
        widget_list.value = [*widget_list.value, WidgetInteractive().key(key)]

    def remove_first():
        widget_list.value = widget_list.value[
            1:]  # specifically removes widget_list[0]

    with sl.Row():
        sl.Button(label="Remove first", on_click=remove_first)
        sl.Button(label="Append another", on_click=add)

    # shows length of widget_list
    sl.Markdown(f"Current length = {len(widget_list.value)}")

    # it doesn't display if you just drop the i there
    for i in widget_list.value:
        sl.display(i)

Run and edit this code snippet at PyCafe

@maartenbreddels maartenbreddels added the documentation Improvements or additions to documentation label May 21, 2024
@maartenbreddels
Copy link
Contributor

        widget_list.value.append(WidgetInteractive())  # add element to list

Also, it's not adviced to mutate the value of a reactive variable, only re-assign. We plan to catch these user-errors in the future: #595

@Jhsmit
Copy link
Contributor

Jhsmit commented May 22, 2024

@maartenbreddels do you have any thoughts on putting solara components in reactive variables?
I would not have thought of doing that, instead my goto solution would be a reactive with a list of strings or dataclass from which components are then generated.
Are there pro/cons?

@rileythai
Copy link
Author

rileythai commented May 23, 2024

I specifically did it this way because I wanted to actually simplify dataclass.

By holding the list of solara components, the states for specific options in the component doesn't need to be elevated into an overarching dataclass -- it's all kept within the component itself, and it can interface directly with the main dataclass that creates its the useful outputs. I can then just mutate this list of components instead of a dataclass structure for cloning/deleting/etc.

In the larger application that this min reprod. demonstrates, I have components where a user sets options (the state) to filter a dataset. These options are only useful within component itself to create it's specific filter object to add to the global filter context.

By containing these options' state inside the component itself, I no longer need a 2nd overarching dataclass that holds the specific options for generating the filter from these options, which we didn't need global access to anyways. Instead, it interfaces directly to the filter context by creating it's relevant filters from the state. Keeping the filter context separate leaves me room to expand with different methods/options of filtering in future.

I'm not sure if this is a technically correct or more complex way of implementing this (I have no web dev experience), but this does have a clear disadvantage for saving user sessions, since it's not elevated into a reproducible/saveable dataclass.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Improvements or additions to documentation
Projects
None yet
Development

No branches or pull requests

3 participants