Selection of multiselect widget is reset after updating options

Summary

The following widgets are part of an image gallery. Each displayed image has a name (e.g. “imageA”) and can be tagged (e.g. “tag1”). The user should be able to filter by name and/or tags. So I used two multiselect widgets. The widget’s options should only show the names/tags that are possible with the current filter selection. The name filter selections should be linked with a logical OR. The selection of the tag filter should be linked with all selected names with a logical AND.
So this is an example of the logical combination: (imageA OR imageB) AND tag1
The problem is: Whenever I select both a name and a tag, the name filter selection is reset.

Steps to reproduce

Code snippet:

import streamlit as st
images=[{"name": "imageA", "tags": ["tag1", "tag2"]},
        {"name": "imageB", "tags": ["tag1"]},
        {"name": "imageC", "tags": []}]

if "name_filter_options" not in st.session_state:
    st.session_state["name_filter_options"] = ["imageA", "imageB", "imageC"]
if "tag_filter_options" not in st.session_state:
    st.session_state["tag_filter_options"] = ["tag1", "tag2"]
if "name_filter" not in st.session_state:
    st.session_state["name_filter"] = []
if "tag_filter" not in st.session_state:
    st.session_state["tag_filter"] = []

if not st.session_state.tag_filter:
    st.session_state.name_filter_options = ["imageA", "imageB", "imageC"]
else:
    for image in images:
        if not set(image["tags"]).intersection(set(st.session_state.tag_filter)):
            st.session_state.name_filter_options.remove(image["name"])

st.multiselect(label="Filter by names", key="name_filter", options=st.session_state.name_filter_options)
st.multiselect(label="Filter by tags", key="tag_filter", options=st.session_state.tag_filter_options)

st.write("Name filter: " + str(st.session_state.name_filter))
st.write("Tag filter: " + str(st.session_state.tag_filter))

Steps to reproduce:

  1. Select “imageA” in first multiselect widget
  2. Select “tag1” in second multiselect widget

Expected behavior:

The first multiselect widget should still have the selected option “imageA” and the second “tag1”.
Similarly, the output should be:

Name filter: [‘imageA’]
Tag filter: [‘tag1’]

Actual behavior:

The selection of the first multiselect widget is reset, but the options are updated.
The output is:

Name filter:
Tag filter: [‘tag1’]

Debug info

  • Streamlit version: 1.19.0
  • Python version: 3.8.10
  • Using Conda? PipEnv? PyEnv? Pex?
  • OS version:
  • Browser version:

Requirements file

Using Conda? PipEnv? PyEnv? Pex? Share the contents of your requirements file here.
Not sure what a requirements file is? Check out this doc and add a requirements file to your app.

Links

  • Link to your GitHub repo:
  • Link to your deployed app:

Additional information

If needed, add any other context about the problem here.

you might want to do the following

import streamlit as st

images=[{"name": "imageA", "tags": ["tag1", "tag2"]},
        {"name": "imageB", "tags": ["tag1"]},
        {"name": "imageC", "tags": []}]

if "name_filter_options" not in st.session_state:
    st.session_state["name_filter_options"] = ["imageA", "imageB", "imageC"]
if "tag_filter_options" not in st.session_state:
    st.session_state["tag_filter_options"] = ["tag1", "tag2"]


name = st.multiselect(label="Filter by names", key = "name", options=st.session_state.name_filter_options)
tag = st.multiselect(label="Filter by tags", key = "tag", options=st.session_state.tag_filter_options)

if not st.session_state.tag:
    st.session_state.name_filter_options = ["imageA", "imageB", "imageC"]
else:
    for image in images:
        if not set(image["tags"]).intersection(set(st.session_state.tag)):
            st.session_state.name_filter_options.remove(image["name"])
1 Like

@mliu Thank you for your quick response.
Unfortunately the name filter options are not updated after setting a tag. So after entering e.g. “tag1” in the tag filter, the name filter should only suggest images with this tag (i.e. “imageA” and “imageB” in the case of “tag1”).

i think that is another issue where “imageC” does not have anything to remove from

    for image in images:
        if not set(image["tags"]).intersection(set(st.session_state.tag)):
            st.session_state.name_filter_options.remove(image["name"])

you can add a condition like

else:
    for image in images:
        if len(image) == 0:
            continue
        if not set(image["tags"]).intersection(set(st.session_state.tag)):
            print(st.session_state.name_filter_options)
            print(image["name"])
            st.session_state.name_filter_options.remove(image["name"])

Unfortunately that doesn’t solve it either.
The problem also occurs when I change the widget’s options by simply overriding the st.session_state.name_filter_options instead of removing items from it.

It’s the same in this code from Shawn_Pereira: Update options of multiselect widget
If a function is selected and then a function is added, the previous selection is reset even if the function still exists as option.

Could it be that whenever the options were changed from the outside, it creates a whole new widget, which of course resets the widget’s selection? If so, is there a way to keep the selection?

Changing any of the parameters used to create a widget will indeed create a new widget. Additionally, the key for a widget is removed from session state when it is destroyed. Hence, changing the options of the multiselect widget causes the state of the widget to be destroyed and reinitialized. You will need to copy the selection information in some way to circumvent this.

Here is a toy example:

import streamlit as st

all_options = ['A','B','C','D','E','F','G']

if '_selected' not in st.session_state:
    st.session_state._possible = all_options
    st.session_state._selected = []
    for option in all_options:
        st.session_state[option] = True

st.session_state.selected = st.session_state._selected

def update(key):
    if st.session_state[key] == True and key not in st.session_state._possible:
        st.session_state._possible.append(key)
        st.session_state._possible.sort()
    elif key in st.session_state._possible:
        st.session_state._possible.remove(key)
        if key in st.session_state._selected:
            st.session_state._selected.remove(key)

for option in all_options:
    st.checkbox(option, key=option, on_change=update, args=[option])

def key_protect():
    st.session_state._selected = st.session_state.selected

st.multiselect('Choose',st.session_state._possible, key='selected', on_change=key_protect)

The point to focus on is copying the widget state to the _selected key any time it is changed and then overwriting the widget state from that copied value at the beginning of the script.

# Get widget state from copied value
st.session_state.selected = st.session_state._selected
# Callback to copy widget state whenever it is changed
def key_protect():
    st.session_state._selected = st.session_state.selected

I have extra lines in there to make sure a selection is removed if it is no longer a valid option, but hopefully you can extract the necessary details for your case.

I hope this is mention in document somewhere, i did not found it so far; i was keep trying to set key session now got to know it is getting destroy on re run, hence was not getting my excepted output. Thank you.

I have written a much more detailed explanation that’s sitting in PR#643 published in the docs.

1 Like