I’m designing a form which allows users to dynamically add inputs.
The goal is to prevent full script reruns when users add new inputs and enter input values by using the new Streamlit fragments decorator.
However, under some conditions, the full script reruns when the user enters an input value.
This script demonstrates a simplified version of the form.
Each button click adds an expander.
Within the expander, the user enters an input value.
There is a popover and checkbox to allow the user to remove the expander.
To cause the undesired full script rerun:
Add two or more food items
Enter an input value in any input following the first food input
–or–
Use the remove popover on any input following the first food input
The issue appears to only occur when operating on a new input after creating multiple inputs.
I suspect it is related to the limitation that fragments cannot call other fragments.
I wonder if there is a way to structure the code to avoid this issue?
The main function run_full_script is huge, it calls a fragment-decorated function and there are other functions that are not fragment-decorated. So whenever there are changes in the states, expects a full rerun to trigger.
Update: Although Streamlit v.137 allows nested fragments, I discovered a bug where Streamlit will incorrectly rerender an element previously created and removed with st.empty.
Steps to Reproduce:
Click Add Favorite Food
Type pizza
Click Remove this food and confirm
Click Add Favorite Food
Type ice cream
During step 5, ice cream will be replaced with pizza and an error will be displayed.
To aid troubleshooting, I’ve added the input key value to the form. Note that the key value reverts to the initial key value when the error occurs. This suggests that Streamlit is maintaining the previous container in memory and redrawing it.
Updated code:
import streamlit as st
import random
import string
from typing import List
def add_food_key_to_session(key):
print("Running add_food_key_to_session()", key)
food_input_keys: List = st.session_state["food_input_keys"]
food_input_keys.append(key)
st.session_state["food_input_keys"] = food_input_keys
def remove_food_input_key(key):
print("Running remove_food_input_key()", key)
food_input_keys: List = st.session_state["food_input_keys"]
print("Before Remove: Session State:", st.session_state, "\n")
food_input_keys.remove(key)
st.session_state["food_input_keys"] = food_input_keys
print("After Remove: Session State:", st.session_state, "\n\n\n\n")
def food_input(key):
print("Running food_input()", key)
food = st.text_input(f"Favorite Food ({key})", key=key)
if food:
st.write(food)
@st.fragment()
def add_edit_remove_food_container(key):
print("Running add_edit_remove_food_container()", key)
container = st.empty()
with container.expander("Food Details", expanded=True):
food_input(key)
remove_food_popover = st.popover(
f"Remove this food? ({key})")
is_remove_food = remove_food_popover.checkbox(
"Confirm", key=f"remove-{key}")
if is_remove_food:
remove_food_input_key(key)
container.empty()
def render_food_containers():
print("Running render_food_containers()")
food_input_keys: List = st.session_state["food_input_keys"]
for key in food_input_keys:
add_edit_remove_food_container(key)
@st.fragment()
def manage_food_button_and_containers():
print("Running manage_food_button_and_containers()")
if st.button("Add Favorite Food"):
key = get_new_key()
add_food_key_to_session("food-" + key)
render_food_containers()
print("Session State:", st.session_state, "\n\n\n\n")
def get_new_key(k=4):
key = "".join(random.choices(
string.ascii_lowercase + string.digits, k=k))
return key
def run_full_script():
print("Running run_full_script")
if "script_run_counter" not in st.session_state:
st.session_state["script_run_counter"] = 1
else:
st.session_state["script_run_counter"] += 1
if "food_input_keys" not in st.session_state:
st.session_state["food_input_keys"] = []
stats_container = st.container()
st.divider()
if st.button("Reset Counter"):
st.session_state["script_run_counter"] = 1
if st.button("Reset form"):
for key in st.session_state.keys():
del st.session_state[key]
st.session_state["script_run_counter"] = 1
st.session_state["food_input_keys"] = []
manage_food_button_and_containers()
with stats_container:
if st.session_state["script_run_counter"] > 1:
st.title(":red[Full Script Run -- You Lose!]")
print("\n", "Full Script Run -- You Lose!", "\n")
st.write(
f'#### Full Script Run Counter: {st.session_state["script_run_counter"]}')
st.write(
"Goal: allow users to dynamically add/edit/delete food inputs without a full script rerun.")
st.divider()
with st.expander("Session State", expanded=False):
st.json(st.session_state)
run_full_script()
Thanks for stopping by! We use cookies to help us understand how you interact with our website.
By clicking “Accept all”, you consent to our use of cookies. For more information, please see our privacy policy.
Cookie settings
Strictly necessary cookies
These cookies are necessary for the website to function and cannot be switched off. They are usually only set in response to actions made by you which amount to a request for services, such as setting your privacy preferences, logging in or filling in forms.
Performance cookies
These cookies allow us to count visits and traffic sources so we can measure and improve the performance of our site. They help us understand how visitors move around the site and which pages are most frequently visited.
Functional cookies
These cookies are used to record your choices and settings, maintain your preferences over time and recognize you when you return to our website. These cookies help us to personalize our content for you and remember your preferences.
Targeting cookies
These cookies may be deployed to our site by our advertising partners to build a profile of your interest and provide you with content that is relevant to you, including showing you relevant ads on other websites.