Best way to hide/show elements dynamically and remember their states

Hello!

I am creating an app that has many dynamically shown/hidden elements with filters. The app works as I want for a checkbox, but the logic is much harder to trace on a multiselect b/c its not just true/false and the on_change callback is 1-step out of sync with the actual elements in the multiselect from my attempts (i.e. when you first enter the item in the multiselect its not in the session state; when you click off and click back and add a second item, now the first is there; etc.).

To get the checkbox to work, I had to unsync some namings on the elements which is why I am curious if this behavior is intended, or if there is a better way to do what I want.

I found from reading that adding elements with a key automatically syncs the element to the session_state variable. However, I found from usage that if the element is no longer visible, it seems to get removed from the session_state variable. This collides with if I want to remember the actual state of the hidden element so that when I show it again the items all stay how I want them to.

To expand, here is the actual scenario/code:

Inside of my libraries, I have an object that contains a display_plan method which is just a collections of streamlit elements

def display_plan(self):
        row = st.columns([0.015,0.25,0.09,0.30,0.30], vertical_alignment = "center")
        with row[0]:
            st.checkbox(label="", key = f"Checkbox {self.name} to menu",value = st.session_state[f"Add {self.name} to menu"],on_change=_record_checkbox_meal_plan_change,kwargs={"label_name":f"Add {self.name} to menu"})
        with row[1]:
            st.markdown(f"### {self.name}")
        with row[2]:
            if self.img is not None:
                with st.popover("Pic"):
                    st.image(self.img)
        with row[3]:
            st.multiselect(label="Add sides from suggested sides", options = self.suggested_sides if self.suggested_sides is not None else [], key = f"SuggestedSides {self.name}")
        with row[4]:
            st.multiselect(label = "Add sides from all recipes", options = [x.name for x in st.session_state["cookbook"]], key = f"AllRecipes {self.name}")

The on_change for the checkbox calls the function

def _record_checkbox_meal_plan_change(label_name):
    if st.session_state[label_name]:
        st.session_state[label_name] = False
    else:
        st.session_state[label_name] = True

Then in the actual app, I loop over the elements with some filters to decide whether or not to show them. Note that the objects to load are in my st.session_state['cookbook'] variable

add_meal_container = st.columns([0.75,0.25])
    with add_meal_container[0]:
        for recipe in st.session_state["cookbook"]:
            cnd1 = recipe.check_filter(name_filter = filter_name)
            cnd1 &= filter_name != ""

            cnd2 = recipe.check_filter(ingredient_filter = filter_ingredients)
            cnd2 &= filter_ingredients != ""

            cnd3 = filter_name == "" and filter_ingredients == ""

            if cnd1 or cnd2 or cnd3:
                recipe.display_plan()
                st.divider()

The issue is that when the filter turns on and thus hides an element, the checkboxes would not get written out, and thus no longer exist, and because the key of the checkbox matched something in my session_state, the session state key was getting removed causing me to lose the “memory” of the checkbox state.

To get around this, I did what is in the code above, namely I use some key name that I then never reference in the actual session_state initialization, and instead basically circumvent the system with my own session_state key.

Any thoughts or input would be great from the team. This seems like something that should be avoidable by adding something to elements like a delete_on_not_exist=True default that we can override.