Skip to content

[Bug]: use for loop to assign multiple modal to multiple button fail #1926

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

Open
xiaogangzhu opened this issue Mar 18, 2025 · 1 comment
Open
Labels
bug Something isn't working

Comments

@xiaogangzhu
Copy link

Component

UI (ui.*)

Severity

P0 - Critical (crash/unusable)

Shiny Version

1.3.0

Python Version

3.12

Minimal Reproducible Example

from shiny import App, render, ui

app_ui = ui.page_fluid(
                        ui.input_action_button("show1", "Show doc1"),
                        ui.input_action_button("show2", "Show doc2"),
                        ui.input_action_button("show3", "Show doc3"),
)
def server(input, output, session):
    for i in range(1,4):
        @reactive.effect
        @reactive.event(input[f"show{i}"])
        def _():
            m = ui.modal(  
                f"This is a somewhat important message.{i}",  
                title="Somewhat important message",  
                easy_close=True,  
            )  
            ui.modal_show(m)  
app = App(app_ui, server)

Behavior

Current: click on each button will display

"This is a somewhat important message.3"

for all modal.

Expect: want to display

"This is a somewhat important message.1"
"This is a somewhat important message.2"
"This is a somewhat important message.3"

for each button

Error Messages (if any)

Environment

ubuntu 20
@xiaogangzhu xiaogangzhu added the bug Something isn't working label Mar 18, 2025
@wch
Copy link
Collaborator

wch commented Mar 18, 2025

This is expected behavior, because of how Python captures the variable i in the function.

Each time you define the function in the loop, Python does not evaluate the value of i; instead it captures that the expression, which happens to contain i. It is only when the function is called that the value of i is fetched. The function is called after the loop has finished and i has the value 3.

Here's an example that doesn't involve Shiny at all. We'll define a function four times in a loop, and store it in a list of functions called fns. The function uses the value i.

fns = []
for i in range(0,4):
    def f():
        print(f"This is message {i}.")
    fns.append(f)


# Now call each of the four functions:
fns[0]()
#> This is message 3.
fns[1]()
#> This is message 3.
fns[2]()
#> This is message 3.
fns[3]()
#> This is message 3.

We called the four separate functions, and each one printed the value 3.

They really are different functions:

fns
#> [<function f at 0x13ed720>, <function f at 0x129ba80>, <function f at 0x11ca2e0>, <function f at 0x1d7f710>]

fns[0] is fns[1]
#> False
fns[1] is fns[2]
#> False
fns[2] is fns[3]
#> False

If we change the value of i, then all of the functions will print the updated value:

i = 100
fns[0]()
#> This is message 100.
fns[1]()
#> This is message 100.
fns[2]()
#> This is message 100.
fns[3]()
#> This is message 100.

So that's what happened: the function looks at the value of i only when it is called, not when the function is defined, and if i changes and then the function is called, it will use the current value of i.

To work around this, you can write a function that creates the reactive.effect, like this (Shinylive app):

from shiny import App, render, ui, reactive

app_ui = ui.page_fluid(
    ui.input_action_button("show1", "Show doc1"),
    ui.input_action_button("show2", "Show doc2"),
    ui.input_action_button("show3", "Show doc3"),
)


def server(input, output, session):
    def create_button_effect(n):
        @reactive.effect
        @reactive.event(input[f"show{n}"])
        def _():
            m = ui.modal(
                f"This is a somewhat important message.{n}",
                title="Somewhat important message",
                easy_close=True,
            )
            ui.modal_show(m)

    for i in range(1, 4):
        create_button_effect(i)


app = App(app_ui, server)

The loop calls create_button_effect(i), and when that function is run, the value of i is evaluated and passed in as the value for n in the function. Each time the create_button_effect() function is called, a new variable scope is created. When the reactive.effect function is created inside of create_button_effect(), it captures that scope. Later, when the reactive effect executes, Python fetches the value of n -- and because each reactive effect was created in a different scope with a different n, each of those reactive effects will report a different value for n.

In other words, in this example, each of the n's refers to a different object. In contrast, in the original example, each of the i's refers to the same object.

@wch wch removed the needs-triage label Mar 18, 2025
@wch wch closed this as completed Mar 18, 2025
@wch wch reopened this Mar 18, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants