[Bug] Weird behavior in Multipage apps with Query-params

Hi,

I noticed a weird behaviour of query_params with the version 0.89
The query params keep resetting to the previous value, the user has to click twice to update the params. Verified that this works properly with versions 0.88 or lower

With version 0.89:
streamlit_0.89_bug

With version 0.88:
streamlit_0.88

My MultiPage framework:

import streamlit as st

class MultiPage:
    def __init__(self):
        self.apps = []
        self.app_names = []
        self.default = ''

    def add_app(self, title, func, args=None):
        self.app_names.append(title)
        self.apps.append({
            "title": title,
            "function": func,
            "args":args
        })

    def run(self):
        # get query_params
        query_params = st.experimental_get_query_params()
        self.default = query_params["app"][0] if "app" in query_params else None
        default_index = self.app_names.index(self.default) if self.default else 0
        # simple radio button for navigation
        app = st.sidebar.radio(
            'Go To',
            self.apps,
            index = default_index,
            format_func=lambda app: app['title'],
            key = 'Navigation')
        # reflect the current app in query_params
        self.default = app['title']
        st.experimental_set_query_params(app=self.default)
        # runs the selected app with passes args
        app['function'](app['args'])

Attaching GIFs for better understanding

Thanks,
Akshansh

Hi @akshanshkmr

st.radio doesn’t return an object, it returns the selection as a string. So you need to resolve the selection to the corresponding app object. The code below works fine, including changing the page directly via the URL app query param. I also quoted and unquoted the URL params just in case.

Having said that, the double click issue remains in v0.89 Hopefully, @Charly_Wargnier can get it looked at by one of the engineers.

import streamlit as st
import urllib

class MultiPage:
    def __init__(self):
        self.apps = []
        self.app_names = []
        self.current_app_name = None

    def add_app(self, title, func, *args, **kwargs):
        self.app_names.append(title)
        self.apps.append({
            "title": title,
            "function": func,
            "args":args,
            "kwargs":kwargs
        })

    def run(self, label='Go To'):
        # make correct app selection from query param
        query_params = st.experimental_get_query_params()
        app_name = urllib.parse.unquote(query_params["app"][0]) if "app" in query_params else None
        self.current_app_name = app_name if app_name in self.app_names else None
        current_app_name_index = self.app_names.index(self.current_app_name) if self.current_app_name else 0
        # configure radio buttons for app navigation
        app_choice = st.sidebar.radio(
            label,
            self.app_names,
            index = current_app_name_index,
            key = 'Navigation')
        app = self.apps[self.app_names.index(app_choice)]
        # update current app name and query param from app choice
        self.current_app_name = app['title']
        # runs the selected app passing args/kwargs
        app['function'](self.current_app_name, *app['args'], **app['kwargs'])
        st.experimental_set_query_params(app=urllib.parse.quote(self.current_app_name))

def app1(title, info=None):
    st.title(title)
    st.write(info)
def app2(title, info=None):
    st.title(title)
    st.write(info)
def app3(title, info=None):
    st.title(title)
    st.write(info)

mp = MultiPage()
mp.add_app('Application 1', app1, info='Hello from App 1')
mp.add_app('Application 2', app2, info='Hello from App 2')
mp.add_app('Application 3', app3, info='Hello from App 3')
mp.run('Launch application')

HTH
Arvindra

Hi @asehmi

Thanks for the updated code,
just to add, I was able to get st.radio to return an object by passing self.apps to the options
image

The quoting and un-quoting of url params is a nice change and i’ll definitely update the framework to include them.

For the Streamlit devs: please look into the double click issue and let me know if something’s unclear

Thanks,
Akshansh

Hi - Yes, I saw that was possible, but thought it best to conform to the docs:

(method) radio: (label: str, options: OptionSequence, index: int = 0, 
format_func=str, key: str | None = None, help: str | None = None, 
on_change: WidgetCallback | None = None, args: WidgetArgs | None = None, 
kwargs: WidgetKwargs | None = None) -> str

Maybe the dev team (via @snehankekre, @Charly_Wargnier) can update the docs if arbitrary collections are indeed returned, which makes sense when a format_func is supplied. (One could always look at the source code in GitHub TBH.)

A

1 Like

Update: I re-built the framework with stateful architecture, it now seems to work properly with v0.89. Sharing the code below

import streamlit as st

class MultiPage:
    def __init__(self):
        self.apps = []
        self.app_names = []

    def add_app(self, title, func, args=None):
        self.app_names.append(title)
        self.apps.append({
            "title": title,
            "function": func,
            "args":args
        })

    def run(self):
        # get query_params
        query_params = st.experimental_get_query_params()
        choice = query_params["app"][0] if "app" in query_params else None
        # common key
        key='Navigation'
        # on_change callback
        def on_change():
            params = st.experimental_get_query_params()
            params['app'] = st.session_state[key]
            st.experimental_set_query_params(**params)
        # update session state
        st.session_state[key] = choice if choice in self.app_names else self.app_names[0]
        appname = st.sidebar.radio('Go To', self.app_names, on_change=on_change, key=key)
        # run the selected app
        for app in self.apps:
            if app['title'] == appname:
                app['function'](app['args'])

Thanks,
Akshansh

1 Like

@akshanshkmr - I liked your update as it leverages the auto-created session state for widgets. Here’s a full working version so the Streamlit community can cut-and-paste and run it. Can you please mark this as the solution?

import streamlit as st

class MultiPage:
    def __init__(self):
        self.apps = []
        self.app_names = []

    def add_app(self, title, func, *args, **kwargs):
        self.app_names.append(title)
        self.apps.append({
            "title": title,
            "function": func,
            "args":args,
            "kwargs": kwargs
        })

    def run(self, label='Go To'):
        # common key
        key='Navigation'

        # get app choice from query_params
        query_params = st.experimental_get_query_params()
        query_app_choice = query_params['app'][0] if 'app' in query_params else None

        # update session state (this also sets the default radio button selection as it shares the key!)
        st.session_state[key] = query_app_choice if query_app_choice in self.app_names else self.app_names[0]

        # callback to update query param from app choice
        def on_change():
            params = st.experimental_get_query_params()
            params['app'] = st.session_state[key]
            st.experimental_set_query_params(**params)
        app_choice = st.sidebar.radio(label, self.app_names, on_change=on_change, key=key)

        # run the selected app
        app = self.apps[self.app_names.index(app_choice)]
        app['function'](app['title'], *app['args'], **app['kwargs'])

def app1(title, info=None):
    st.title(title)
    st.write(info)
def app2(title, info=None):
    st.title(title)
    st.write(info)
def app3(title, info=None):
    st.title(title)
    st.write(info)

mp = MultiPage()
mp.add_app('Application 1', app1, info='Hello from App 1')
mp.add_app('Application 2', app2, info='Hello from App 2')
mp.add_app('Application 3', app3, info='Hello from App 3')
mp.run('Launch application')

Thanks,
Arvindra

Thanks for reporting @akshanshkmr and @asehmi! :raised_hands:

We’ll liaise with Devs and come back to you shortly on this.

Please be aware the team is attending an off-site event this week, so we may be slightly slower to feedback :slight_smile:

Thanks,
Charly

1 Like