How to Do Partial Rerun for This Dynamic Form Using Fragments?

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:

  1. Add two or more food items
  2. Enter an input value in any input following the first food input
    –or–
  3. 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?

Python v3.10
Streamlit v1.33
GitHub repo: https://github.com/TerraX3000/streamlit_fragment_test
Runs locally

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"]
    food_input_keys.remove(key)
    st.session_state["food_input_keys"] = food_input_keys


def food_input(key):
    print("Running food_input()", key)
    food = st.text_input("Favorite Food", key=key)
    if food:
        st.write(food)


@st.experimental_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("Remove this food?")
        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.experimental_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")


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()

1 Like

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.