Streamlit App refreshes and resets widget values to default when user edits a value in an input widget

I have just started using streamlit last week and i am facing the issue as specified in the title. What my app is basically doing right now is taking a json input which contains a list of models with a dict for each model containing the model name, type, and a dict of params. i am asking the user to select the models they want from a multiselect, enter the instances for each model, and then allow them to edit the name and params for every instance of every model they select. however, whenever a user edits the value in the input widget for the model name or params, it refreshes and goes back to selecting models and instances.
i saw cache_data and session_state could be used to handle something like this but i am not sure how to exactly do it.

Can you make some simple sample data (just a simple dict or dataframe inline) and show how you are currently implementing your selections? Just showings a few layers in your sequence of selections in an executable snippet would help. That will make it easier to explain the current behavior you see and how to modify it to get what you want.

In this, whenever the user edits the value of any of the widgets for input, it refreshes and goes back to the state before the user clicks on the " Proceed to enter parameters." button.

Certainly, this is a skeleton form of what I am doing to take in input

def input(options, options_details,adv_params_all_opts):
    selected_opts = st.multiselect('Please select the options u would like to use:', options, key='select_models')
    num_instances = {}
    st.write("Enter the number of instances you would for each option:")
    for opt in selected_opts:
        num_instances[opt] = st.number_input(label=selected_opts[i], min_value=1, step=1, key=f'num_instances_model_{i}')
    if st.button("Proceed to enter parameters."):
        user_modified_list = []
        columns = st.columns(3)
        count = 0
        for i in range(len(selected_opts)):
            selected_opt = selected_opts[i]
            for j in range(num_instances[selected_opt]):
                with columns[count]:
                    selected_option_idx = next((idx for (idx,d) in enumerate(options_details['options']) if d['type'] == selected_opt), None)
                    selected_opt_deets = options_details['options'][selected_option_idx]
                    with st.form(f"Option_{selected_opt}_Instance_{j}"):
                        user_modified_opt = {}
                        user_modified_opt['type'] = selected_opt_deets['type']
                        user_modified_opt['name'] = st.text_input('Name', selected_opt_deets['name'], key=f"model_name_{j}_{i}")
                        opt_params = {}
                        adv_params = {}
                        for k, v in selected_opt_deets['params'].items():
                            if k in adv_params_all_opts[selected_opt]:
                                adv_params[k] = v
                            else:
                                opt_params[k] = v
                        # params_input is a function which takes list of params and lists, provides the user with a widget for each param accdg to the data type of the respective value and returns the two respective lists with the data from those widgets
                        # example (inside a for loop of number of parameters): st.number_input(label=param_keys[k], value=param_vals[k], step=1, key=f"int_input_{param_keys[k]}_{k}_{j}_{i}")
                        # each widget in params_input has unique key based on the param key, i, j.
                        opt_params_keys, opt_params_vals = params_input(list(opt_params.keys()), list(opt_params.values()), i, j)
                        params = {k: v for k, v in zip(opt_params_keys, opt_params_vals)}
                        with st.expander("Show Advanced Params"):
                            adv_param_keys, adv_param_vals = params_input(list(adv_params.keys()), list(adv_params.values()), i, j)
                            for k in range(len(adv_param_keys)):
                                params[adv_param_keys[k]] = adv_param_vals[k]
                        user_modified_opt['params'] = params
                        st.form_submit_button("Save this.", on_click=user_modified_list.append(user_modified_opt))
                    count+=1
                    if count == 3:
                        count = 0
        if st.button("Next Step"):
            with open('user_input.json', 'w') as f:
                user_input = {'options': user_modified_list}
                json.dump(user_input, f, indent=4)
            switch_page("Next Page")

Thank you!

Nesting things inside a button gets a lot people. Buttons return true on the page load resulting from their click and then immediately go back to false. If you nest any kind of widget inside a button, then as soon as the user interacts with it (or anything else on the page), the page reloads with the button being false (and no more anything nested under that button).

Check out this blog post for some explanation.

I also have a proposal for some more intensive explanation about buttons in the knowledge base if you want to preview it. This comment has an expander element with a preview of the resultant page.

1 Like

Got it, thanks a ton! It was right there but I somehow didn’t think that was the reason, pointing this out was immensely helpful.

I was able to make it work without a button, however, I am curious as to what would you suggest would be the best work around in this case to reproduce the effect of the button while being stateful?

Use one of the options listed in the links. You can store information in session state to save whether or not a button has been clicked. Then you condition on that value in session state instead of directly on the button.

1 Like

Got it, will try that out some time. Thank you so much!

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.