How to prevent streamlit app from rerunning when widgets are interacted with?

I am developing a Streamlit app within Snowflake.

It is very basic. I have a button that runs a db query, gets a dataframe, and shows it in a data editor widget. I quickly discovered that as soon as I made one change in the data editor, it would disappear. I read that this is because the app reruns automatically when a widget is interacted with.

So I tried putting the data editor into a form. Now, I can update the data editor, but when I click my form submission button, the whole form disappears. Because the app reruns automatically when a form is submitted.

This is frustrating to no end. What I would love is to be able to configure the app to only rerun when I want it to (via the rerun() function) , rather than every time a widget is clicked or form is submitted. Is that possible? It really should be.

Thank you.

Hey William, welcome to the forums!

Can you post a small code example showing the problem?

Hello Thiago. Thank you for replying so quickly. Here’s the entirety of my app. I’ve removed actual DB operations and replaced them with dummy dataframes, but the disappearing widget phenomenon is still present. Thank you for your help.

# Import python packages
import datetime as dt;
import streamlit as st;
import pandas as pd;
import numpy as np;
from snowflake.snowpark.context import get_active_session;

#Set page config.
st.set_page_config(page_title="My App",layout="wide",initial_sidebar_state="expanded");

#Get rid of vertical padding.
st.markdown("""<style>.block-container {padding-top: 0rem;padding-bottom: 0rem;padding-left: 3rem;padding-right: 0rem;}</style>""", unsafe_allow_html=True);

#Get active session.
session = get_active_session();

#Title.
st.write("My App");

#Simulating a db query for selectbox options.
dfOptions=pd.DataFrame({"HELLO" : ["Foo","Bar","Buzz"]}); 
option=st.selectbox("Option",dfOptions,index=None,key="optionsInput");

search=st.button("search");
if(search):
    #Simulating another db query that would run on the button click using the selectbox value as a binding variable.
    dfData = pd.DataFrame({'A' : ['spam', 'eggs', 'spam', 'eggs'] * 6,
                       'B' : ['alpha', 'beta', 'gamma'] * 8,
                       'C' : [np.random.choice(pd.date_range(dt.datetime(2013,1,1),dt.datetime(2013,1,3))) for i in range(24)],
                       'D' : np.random.randn(24),
                       'E' : np.random.random_integers(0,4,24)});
    
    with st.form("myForm"):
        st.data_editor(dfData, key="editor");
        submitChanges = st.form_submit_button("Submit Changes");

        if(submitChanges):
            #Simulate writing back to db the updated rows in the data editor.
            #But then, the data editor disappears.  My question is: how to prevent this?
            dict=st.session_state.editor["edited_rows"];
            #Won't see this because form disappears.
            st.write(dict);
            st.success("Update successful");


Not to steal Thiago’s thunder, but I have a couple of suggestions:

The basic problem is that if a button is pressed, and then you press some other button (or interact with another widget), the button immediately goes back into “unpressed” mode.

One super simple fix is to swap st.button for st.toggle – this acts like a button, but it stays “on” until you explicitly turn it off.

[Less importantly, one tricky thing about your example is that the dataframe will be regenerated with random values every time you interact with it, so in my example below I’m putting the data in st.session_state so that it is generated once and not regenerated every time – you could do something similar with @st.cache_data if you wanted]

Another option that seems nice in this case is to use st.dialog to edit the data if you press the button:

# Import python packages
import datetime as dt
from time import sleep

import numpy as np
import pandas as pd
import streamlit as st

# Simulating a db query for selectbox options.
dfOptions = pd.DataFrame({"HELLO": ["Foo", "Bar", "Buzz"]})
option = st.selectbox("Option", dfOptions, index=None, key="optionsInput")

if "data" not in st.session_state:
    st.session_state.data = pd.DataFrame(
        {
            "A": ["spam", "eggs", "spam", "eggs"] * 6,
            "B": ["alpha", "beta", "gamma"] * 8,
            "C": [
                np.random.choice(
                    pd.date_range(dt.datetime(2013, 1, 1), dt.datetime(2013, 1, 3))
                )
                for i in range(24)
            ],
            "D": np.random.randn(24),
            "E": np.random.random_integers(0, 4, 24),
        }
    )


@st.dialog("Search")
def search_df():
    dfData = st.session_state.data

    with st.form("myForm"):
        st.data_editor(dfData, key="editor")
        submitChanges = st.form_submit_button("Submit Changes")

        if submitChanges:
            # Simulate writing back to db the updated rows in the data editor.
            dict = st.session_state.editor["edited_rows"]
            st.write(dict)
            st.success("Update successful")
            sleep(0.5) # Just delay the rerun so you can see the success
            st.rerun()


search = st.button("search")
if search:
    search_df()

Hi Zachary,

Thank you for the example. I gave it a try.

I do not think this covers my use case exactly. In your example, I can see the data editor for as long as I need to make the updates, which is also possible by putting it into a form as in my code.

But the user would only be able to see their changes after submitting your form for as long as the app is sleeping, after which the editor disappears.

I’m looking to have the app only conduct the DB operations and success message display that are behind the form submit button, then just stop and do nothing further unless it is interacted with again, leaving the editor visible.

If that is just not possible currently, then the second best situation would be to have the rerun of the app bring us to exactly the same presence of all widgets on the screen as when the form submit button was clicked. So, if it disappears for a split second then reappears, that’s tolerable.

I feel like this might be possible with some clever usage of the session state. But I must admit I am a brand new streamlit developer and have no idea how to make it happen.

Also, apologies if the random dataframe data caused confusion. Its not germane to my problem, its just a dummy example I grabbed from the internet without thinking too much about it. In my real use case, the data in the editor would just come directly from the database and not be randomly generated.

Did you try switching st.button with st.toggle? It sounds like that would accomplish what you’re looking for.

Zachary,

I tried replacing the button with a toggle as you suggested.

You are correct that when the app reruns after the form is submitted, the data editor does come back.

It is unfortunate that we have to use a toggle and not a button, because its kind of an unusual/unintuitive thing to ask a user to flip a toggle instead of clicking a button to make an action happen. But it is at least a workaround that works. Is it possible to style a toggle to look like a button to make this easier for the user to understand?

Something I notice now, though, is that the app will not rerun unless the rerun() command is added to the form submit button logic. This seems like unexpected behavior, right? Do you have any idea why the change of the input widget type would affect whether the app would rerun on the form submission?

I’ve included the code below. The only thing I changed apart from comments is making the button a toggle and taking the random numbers out of the dataframe.

Thank you

# Import python packages
import datetime as dt;
import streamlit as st;
import pandas as pd;
import numpy as np;
import time;
from snowflake.snowpark.context import get_active_session;

#Set page config.
st.set_page_config(page_title="My App",layout="wide",initial_sidebar_state="expanded");

#Get rid of vertical padding.
st.markdown("""<style>.block-container {padding-top: 0rem;padding-bottom: 0rem;padding-left: 3rem;padding-right: 0rem;}</style>""", unsafe_allow_html=True);

#Get active session.
session = get_active_session();

#Title.
st.write("My App");

#Simulating a db query for selectbox options.
dfOptions=pd.DataFrame({"HELLO" : ["Foo","Bar","Buzz"]}); 
option=st.selectbox("Option",dfOptions,index=None,key="optionsInput");

search=st.toggle("search");
if(search):
    #Simulating another db query that would run on the button click using the selectbox value as a binding variable.
    dfData = pd.DataFrame({'A' : ['spam', 'eggs', 'spam', 'eggs'] * 6,
                            'B' : ['alpha', 'beta', 'gamma'] * 8,
                            'C' : ['spam', 'eggs', 'spam', 'eggs'] * 6,
                            'D' : ['alpha', 'beta', 'gamma'] * 8,
                            'E' : ['spam', 'eggs', 'spam', 'eggs'] * 6});
                       
    
    with st.form("myForm"):
        st.data_editor(dfData, key="editor");
        submitChanges = st.form_submit_button("Submit Changes");

        if(submitChanges):
            #Simulate writing back to db the updated rows in the data editor.
            dict=st.session_state.editor["edited_rows"];
            st.write(dict);
            st.success("Update successful");           
            #Without this, the form does not rerun the app.  Unexpected?            
            # st.rerun();

If you want something that looks like a button, but behaves like a toggle, you might try this 🔛 Stateful Button - streamlit-extras

In terms of the rerun, you can think of a simple app:

import streamlit as st

if "x" not in st.session_state:
    st.session_state.x = 1
st.write("x =", st.session_state.x)

cols = st.columns(3)
if cols[0].button("without rerun"):
    st.session_state.x = 2

if cols[1].button("with rerun"):
    st.session_state.x = 2
    st.rerun()


def callback():
    st.session_state.x = 2


cols[2].button("with callback", on_click=callback)

st.write("x =", st.session_state.x)

The normal behavior of a widget is that it does make the app rerun, but the app reruns from top to bottom, and the updated state of the widget only affects the things afterwards in the app.

So, if you press the first button, you get:
x = 1
buttons
x = 2

If you clear the session state (hit “C”) and then instead press the second button, it updates the session state and then makes the whole app rerun, so you immediately see the new x value even at the top, so you get

x = 2
buttons
x = 2

If you clear the session state and hit the 3rd button, you get the same behavior as the second. That’s because callbacks behave as if they were at the very top of the script, so anything they change will be shown across the whole app, so you still get:

x = 2
buttons
x = 2

It just seems unpredictable to me when the app will decide to rerun or not. It says on this page that button clicks will cause the app to rerun. But your example shows that this does not always happen.

It always causes the page to rerun, but it doesn’t make the changes in a widget affect the parts of the code before the widget. If you want to affect what happens before the widget in your code, you either need to use st.rerun, or use a callback so that the effects of interacting with the widget happen before the rest of the code.

To prove that it always does rerun, we can just add the current timestamp:

from datetime import datetime

import streamlit as st

if "x" not in st.session_state:
    st.session_state.x = 1
st.write("x =", st.session_state.x)

st.write("The current time is", datetime.now())

cols = st.columns(3)
if cols[0].button("without rerun"):
    st.session_state.x = 2

if cols[1].button("with rerun"):
    st.session_state.x = 2
    st.rerun()


def callback():
    st.session_state.x = 2


cols[2].button("with callback", on_click=callback)

st.write("x =", st.session_state.x)

You’ll notice that the current time always changes when you click any button. The question isn’t whether the app will rerun, but whether the effects of the button being pressed will affect parts of the script that are before the button.

You can reaad more about that further down on that page, here Button behavior and examples - Streamlit Docs

Thank you for the explanation. Still figuring all this out.

Hi William,

You can check this video from TechWithTim to understand how session_state works : Streamlit Mini Course - Make Websites With ONLY Python

It helped me a lot to build an app within my company. Hope that will help you too !

Anthony