pythonpy-shiny

How to show two outputs of the same function without running it twice?


I have this function that iterates over my data and generates two outputs (in the example, the function check(). Now I want to show both outputs on different cards. AFAIK the card ID has to be the same as the function that generates the output. In my example, the function check() is run twice, which is very inefficient (in my real data this is the function generating the most computational load). Is there a way to run this function only once, but still using both outputs on different cards in shiny for Python?

MRE:

from shiny import App, render, ui, reactive
from pathlib import Path


app_ui = ui.page_fillable(
    ui.layout_sidebar(
        ui.sidebar(
            ui.input_text("numbers", "Number you want to check", value="1,2,3,4,5,6,7,8"),
            ui.input_action_button("check_numbers", "Check your numbers")
        ),
        ui.output_ui("numbers_frames")
    )
)


def server(input, output, session):

    @render.ui
    @reactive.event(input.check_numbers)
    def numbers_frames():

        return ui.TagList(
                    ui.layout_columns(
                        ui.card(
                            ui.card_header("even"),
                            ui.output_text("even"),
                        ),
                        ui.card(
                            ui.card_header("odd"),
                            ui.output_text("odd"),
                        ),
                    )
                )

    @reactive.event(input.check_numbers)
    def check():

        even = []
        odd = []

        for number in input.numbers().split(','):
            number_int = int(number.strip())
            if number_int % 2 == 0:
                even.append(str(number_int))
            else:
                odd.append(str(number_int))

        print("check() has been exectuted")

        return (even, odd)


    @output
    @render.text
    def even():

        even_output = check()[0]

        return ','.join(even_output)

    @output
    @render.text
    def odd():

        odd_output = check()[1]

        return ','.join(odd_output)


src_dir = Path(__file__).parent / "src"
app = App(app_ui, server, static_assets=src_dir)

Solution

  • Rewrite your check() function such that it modifies a reactive.value() which contains the odd and the even numbers from the input. check() is decorated by reactive.effect() and reactive.event(), where the event is input.check_numbers. Then check() only gets executed once when the user clicks the button. And then, inside the render.text(), you display the current content of the reactive.value().

    from shiny import App, render, ui, reactive
    from pathlib import Path
    
    app_ui = ui.page_fillable(
        ui.layout_sidebar(
            ui.sidebar(
                ui.input_text("numbers", "Number you want to check", value="1,2,3,4,5,6,7,8"),
                ui.input_action_button("check_numbers", "Check your numbers")
            ),
            ui.output_ui("numbers_frames")
        )
    )
    
    def server(input, output, session):
        
        outputNumbers = reactive.value({})
    
        @render.ui
        @reactive.event(input.check_numbers)
        def numbers_frames():
            return ui.TagList(
                        ui.layout_columns(
                            ui.card(
                                ui.card_header("even"),
                                ui.output_text("even"),
                            ),
                            ui.card(
                                ui.card_header("odd"),
                                ui.output_text("odd"),
                            ),
                        )
                    )
    
        @reactive.effect()
        @reactive.event(input.check_numbers)
        def check():
            nums = [n for n in input.numbers().split(',') if n]
            
            evenNums = [str(num) for num in nums if int(num.strip()) % 2 == 0]
            oddNums = [str(num) for num in nums if int(num.strip()) % 2 != 0]
            
            outputNumbers.set({'odd': ','.join(oddNums), 'even': ','.join(evenNums)})       
            
            print("check() has been executed")
    
        @output
        @render.text
        def even():
            return outputNumbers()['even']
    
        @output
        @render.text
        def odd():
            return outputNumbers()['odd']
    
    src_dir = Path(__file__).parent / "src"
    app = App(app_ui, server, static_assets=src_dir)